From cb9327e03020b7cd6965c73cf7db2177eb46e7d9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 10 Jan 2018 13:09:22 -0800 Subject: [PATCH 001/157] Add entity file sync and domain content backups --- .clang-format | 10 +- assignment-client/CMakeLists.txt | 1 + assignment-client/src/Agent.cpp | 1 - .../src/entities/EntityServer.cpp | 2 - assignment-client/src/entities/EntityServer.h | 13 +- assignment-client/src/octree/OctreeServer.cpp | 201 ++++++------ assignment-client/src/octree/OctreeServer.h | 21 +- .../src/scripts/EntityScriptServer.cpp | 2 +- domain-server/CMakeLists.txt | 12 +- .../src/DomainContentBackupManager.cpp | 303 ++++++++++++++++++ .../src/DomainContentBackupManager.h | 88 +++++ domain-server/src/DomainServer.cpp | 213 +++++++++++- domain-server/src/DomainServer.h | 28 +- interface/src/Application.cpp | 8 +- interface/src/ui/DomainConnectionModel.cpp | 2 +- libraries/entities/src/EntityTree.cpp | 10 + libraries/image/CMakeLists.txt | 3 +- libraries/networking/src/LimitedNodeList.cpp | 1 + libraries/networking/src/ThreadedAssignment.h | 6 +- libraries/networking/src/udt/PacketHeaders.h | 7 + libraries/networking/src/udt/Socket.cpp | 4 +- libraries/octree/CMakeLists.txt | 1 + libraries/octree/src/Octree.cpp | 54 +++- libraries/octree/src/Octree.h | 14 +- libraries/octree/src/OctreePersistThread.cpp | 102 +++--- libraries/octree/src/OctreePersistThread.h | 11 +- libraries/octree/src/OctreeUtils.cpp | 77 +++++ libraries/octree/src/OctreeUtils.h | 23 ++ libraries/shared/src/SharedUtil.cpp | 6 +- libraries/shared/src/SharedUtil.h | 1 + 30 files changed, 1026 insertions(+), 199 deletions(-) create mode 100644 domain-server/src/DomainContentBackupManager.cpp create mode 100644 domain-server/src/DomainContentBackupManager.h diff --git a/.clang-format b/.clang-format index f000a27017..507b1eb232 100644 --- a/.clang-format +++ b/.clang-format @@ -1,12 +1,12 @@ Language: Cpp Standard: Cpp11 -BasedOnStyle: "Chromium" +BasedOnStyle: "Chromium" ColumnLimit: 128 IndentWidth: 4 UseTab: Never BreakBeforeBraces: Custom -BraceWrapping: +BraceWrapping: AfterEnum: true AfterClass: false AfterControlStatement: false @@ -21,11 +21,11 @@ BraceWrapping: AccessModifierOffset: -4 -AllowShortFunctionsOnASingleLine: InlineOnly -BreakConstructorInitializers: BeforeColon +AllowShortFunctionsOnASingleLine: InlineOnly +BreakConstructorInitializers: BeforeColon BreakConstructorInitializersBeforeComma: true IndentCaseLabels: true -ReflowComments: false +ReflowComments: false Cpp11BracedListStyle: false ContinuationIndentWidth: 4 ConstructorInitializerAllOnOneLineOrOnePerLine: false diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index c73e8e1d34..3de4c5fd3f 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -6,6 +6,7 @@ setup_hifi_project(Core Gui Network Script Quick WebSockets) if (APPLE) set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") endif () +set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "/testing/") setup_memory_debugger() diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index a42b78a6fa..10b8d44545 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -340,7 +340,6 @@ void Agent::scriptRequestFinished() { request->deleteLater(); } - void Agent::executeScript() { _scriptEngine = scriptEngineFactory(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload); diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index f72832f902..e394884dc2 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -116,7 +116,6 @@ void EntityServer::beforeRun() { void EntityServer::entityCreated(const EntityItem& newEntity, const SharedNodePointer& senderNode) { } - // EntityServer will use the "special packets" to send list of recently deleted entities bool EntityServer::hasSpecialPacketsToSend(const SharedNodePointer& node) { bool shouldSendDeletedEntities = false; @@ -277,7 +276,6 @@ int EntityServer::sendSpecialPackets(const SharedNodePointer& node, OctreeQueryN return totalBytes; } - void EntityServer::pruneDeletedEntities() { EntityTreePointer tree = std::static_pointer_cast(_tree); if (tree->hasAnyDeletedEntities()) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index 05404b28c8..4d3f1ee89f 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -30,7 +30,6 @@ struct ViewerSendingStats { class SimpleEntitySimulation; using SimpleEntitySimulationPointer = std::shared_ptr; - class EntityServer : public OctreeServer, public NewlyCreatedEntityHook { Q_OBJECT public: @@ -38,7 +37,7 @@ public: ~EntityServer(); // Subclasses must implement these methods - virtual std::unique_ptr createOctreeQueryNode() override ; + virtual std::unique_ptr createOctreeQueryNode() override; virtual char getMyNodeType() const override { return NodeType::EntityServer; } virtual PacketType getMyQueryMessageType() const override { return PacketType::EntityQuery; } virtual const char* getMyServerName() const override { return MODEL_SERVER_NAME; } @@ -82,12 +81,12 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; - static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m - static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h - int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m - int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h + static const int DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 45 * 60 * 1000; // 45m + static const int DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = 60 * 60 * 1000; // 1h + int _MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MINIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 45m + int _MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS = DEFAULT_MAXIMUM_DYNAMIC_DOMAIN_VERIFICATION_TIMER_MS; // 1h QTimer _dynamicDomainVerificationTimer; void startDynamicDomainVerification(); }; -#endif // hifi_EntityServer_h +#endif // hifi_EntityServer_h diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 42494ea7ee..e78f9f108b 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -33,6 +33,10 @@ #include #include +#include + +Q_LOGGING_CATEGORY(octree_server, "hifi.octree-server") + int OctreeServer::_clientCount = 0; const int MOVING_AVERAGE_SAMPLE_COUNTS = 1000; @@ -84,6 +88,8 @@ int OctreeServer::_longProcessWait = 0; int OctreeServer::_shortProcessWait = 0; int OctreeServer::_noProcessWait = 0; +static const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; + void OctreeServer::resetSendingStats() { _averageLoopTime.reset(); @@ -202,7 +208,6 @@ void OctreeServer::trackPacketSendingTime(float time) { } } - void OctreeServer::trackProcessWaitTime(float time) { const float MAX_SHORT_TIME = 10.0f; const float MAX_LONG_TIME = 100.0f; @@ -283,8 +288,6 @@ void OctreeServer::initHTTPManager(int port) { _httpManager = new HTTPManager(QHostAddress::AnyIPv4, port, documentRoot, this, this); } -const QString PERSIST_FILE_DOWNLOAD_PATH = "/models.json.gz"; - bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler) { #ifdef FORCE_CRASH @@ -922,87 +925,6 @@ void OctreeServer::handleOctreeDataNackPacket(QSharedPointer me } } -void OctreeServer::handleOctreeFileReplacement(QSharedPointer message) { - if (!_isFinished && !_isShuttingDown) { - // these messages are only allowed to come from the domain server, so make sure that is the case - auto nodeList = DependencyManager::get(); - if (message->getSenderSockAddr() == nodeList->getDomainHandler().getSockAddr()) { - // it's far cleaner to load up the new content upon server startup - // so here we just store a special file at our persist path - // and then force a stop of the server so that it can pick it up when it relaunches - if (!_persistAbsoluteFilePath.isEmpty()) { - replaceContentFromMessageData(message->getMessage()); - } else { - qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; - } - } else { - qDebug() << "Received an octree file replacement that was not from our domain server - refusing to process"; - } - } -} - -// Message->getMessage() contains a QByteArray representation of the URL to download from -void OctreeServer::handleOctreeFileReplacementFromURL(QSharedPointer message) { - qInfo() << "Received request to replace content from a url"; - if (!_isFinished && !_isShuttingDown) { - // This call comes from Interface, so we skip our domain server check - // but confirm that we have permissions to replace content sets - if (DependencyManager::get()->getThisNodeCanReplaceContent()) { - if (!_persistAbsoluteFilePath.isEmpty()) { - // Convert message data into our URL - QString url(message->getMessage()); - QUrl modelsURL = QUrl(url, QUrl::StrictMode); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest request(modelsURL); - QNetworkReply* reply = networkAccessManager.get(request); - connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() { - QNetworkReply::NetworkError networkError = reply->error(); - if (networkError == QNetworkReply::NoError) { - QByteArray contents = reply->readAll(); - replaceContentFromMessageData(contents); - } else { - qDebug() << "Error downloading JSON from specified file"; - } - }); - } else { - qDebug() << "Cannot perform octree file replacement since current persist file path is not yet known"; - } - } - } -} - -void OctreeServer::replaceContentFromMessageData(QByteArray content) { - //Assume we have compressed data - auto compressedOctree = content; - QByteArray jsonOctree; - - bool wasCompressed = gunzip(compressedOctree, jsonOctree); - if (!wasCompressed) { - // the source was not compressed, assume we were sent regular JSON data - jsonOctree = compressedOctree; - } - // check the JSON data to verify it is an object - if (QJsonDocument::fromJson(jsonOctree).isObject()) { - if (!wasCompressed) { - // source was not compressed, we compress it before we write it locally - gzip(jsonOctree, compressedOctree); - } - // write the compressed octree data to a special file - auto replacementFilePath = _persistAbsoluteFilePath.append(OctreePersistThread::REPLACEMENT_FILE_EXTENSION); - QFile replacementFile(replacementFilePath); - if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { - // we've now written our replacement file, time to take the server down so it can - // process it when it comes back up - qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; - setFinished(true); - } else { - qWarning() << "Could not write replacement octree data to file - refusing to process"; - } - } else { - qDebug() << "Received replacement octree file that is invalid - refusing to process"; - } -} - bool OctreeServer::readOptionBool(const QString& optionName, const QJsonObject& settingsSectionObject, bool& result) { result = false; // assume it doesn't exist bool optionAvailable = false; @@ -1119,7 +1041,19 @@ void OctreeServer::readConfiguration() { _persistFilePath = getMyDefaultPersistFilename(); } + // If persist filename does not exist, let's see if there is one beside the application binary + // If there is, let's copy it over to our target persist directory + QDir persistPath { _persistFilePath }; + _persistAbsoluteFilePath = persistPath.absolutePath(); + + if (persistPath.isRelative()) { + // if the domain settings passed us a relative path, make an absolute path that is relative to the + // default data directory + _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + } + qDebug() << "persistFilePath=" << _persistFilePath; + qDebug() << "persisAbsoluteFilePath=" << _persistAbsoluteFilePath; _persistAsFileType = "json.gz"; @@ -1200,20 +1134,90 @@ void OctreeServer::run() { } void OctreeServer::domainSettingsRequestComplete() { + if (_state != OctreeServerState::WaitingForDomainSettings) { + qCWarning(octree_server) << "Received domain settings after they have already been received"; + return; + } + + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); + packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); + + packetReceiver.registerListener(PacketType::OctreeDataFileReply, this, "handleOctreeDataFileReply"); + + qDebug(octree_server) << "Received domain settings"; + + readConfiguration(); + + _state = OctreeServerState::WaitingForOctreeDataNegotation; + + auto nodeList = DependencyManager::get(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + auto packet = NLPacket::create(PacketType::OctreeDataFileRequest, -1, true, false); + + OctreeUtils::RawOctreeData data; + qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath; + if (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { + qCDebug(octree_server) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + packet->writePrimitive(true); + auto id = data.id.toRfc4122(); + packet->write(id); + packet->writePrimitive(data.version); + } else { + qCWarning(octree_server) << "No octree data found"; + packet->writePrimitive(false); + } + + qCDebug(octree_server) << "Sending request for octree data to DS"; + nodeList->sendPacket(std::move(packet), domainHandler.getSockAddr()); +} + +void OctreeServer::handleOctreeDataFileReply(QSharedPointer message) { + bool includesNewData; + message->readPrimitive(&includesNewData); + QByteArray replaceData; + if (includesNewData) { + replaceData = message->readAll(); + qDebug() << "Got reply to octree data file request, new data sent"; + } else { + qDebug() << "Got reply to octree data file request, current entity data is sufficient"; + + OctreeUtils::RawOctreeData data; + qCDebug(octree_server) << "Reading octree data from" << _persistAbsoluteFilePath; + if (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { + if (data.id.isNull()) { + qCDebug(octree_server) << "Current octree data has a null id, updating"; + data.id = QUuid::createUuid(); + data.version = 0; + + QFile file(_persistAbsoluteFilePath); + if (file.open(QIODevice::WriteOnly)) { + auto entityData = data.toByteArray(); + file.write(entityData); + file.close(); + } else { + qCDebug(octree_server) << "Failed to update octree data"; + } + } + } + } + beginRunning(replaceData); +} + +void OctreeServer::beginRunning(QByteArray replaceData) { + if (_state == OctreeServerState::Running) { + qCWarning(octree_server) << "Server is already running"; + return; + } + + _state = OctreeServerState::Running; auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer }); - auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket"); - packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket"); - packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacement"); - packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURL"); - - readConfiguration(); - beforeRun(); // after payload has been processed connect(nodeList.data(), SIGNAL(nodeAdded(SharedNodePointer)), SLOT(nodeAdded(SharedNodePointer))); @@ -1233,17 +1237,6 @@ void OctreeServer::domainSettingsRequestComplete() { // if we want Persistence, set up the local file and persist thread if (_wantPersist) { - // If persist filename does not exist, let's see if there is one beside the application binary - // If there is, let's copy it over to our target persist directory - QDir persistPath { _persistFilePath }; - _persistAbsoluteFilePath = persistPath.absolutePath(); - - if (persistPath.isRelative()) { - // if the domain settings passed us a relative path, make an absolute path that is relative to the - // default data directory - _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); - } - static const QString ENTITY_PERSIST_EXTENSION = ".json.gz"; // force the persist file to end with .json.gz @@ -1328,7 +1321,7 @@ void OctreeServer::domainSettingsRequestComplete() { // now set up PersistThread _persistThread = new OctreePersistThread(_tree, _persistAbsoluteFilePath, _backupDirectoryPath, _persistInterval, - _wantBackup, _settings, _debugTimestampNow, _persistAsFileType); + _wantBackup, _settings, _debugTimestampNow, _persistAsFileType, replaceData); _persistThread->initialize(true); } diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 0eba914064..6f77920ee0 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -27,8 +27,18 @@ #include "OctreeServerConsts.h" #include "OctreeInboundPacketProcessor.h" +#include + +Q_DECLARE_LOGGING_CATEGORY(octree_server) + const int DEFAULT_PACKETS_PER_INTERVAL = 2000; // some 120,000 packets per second total +enum class OctreeServerState { + WaitingForDomainSettings, + WaitingForOctreeDataNegotation, + Running +}; + /// Handles assignments of type OctreeServer - sending octrees to various clients. class OctreeServer : public ThreadedAssignment, public HTTPRequestHandler { Q_OBJECT @@ -36,6 +46,8 @@ public: OctreeServer(ReceivedMessage& message); ~OctreeServer(); + OctreeServerState _state { OctreeServerState::WaitingForDomainSettings }; + /// allows setting of run arguments void setArguments(int argc, char** argv); @@ -137,8 +149,9 @@ private slots: void domainSettingsRequestComplete(); void handleOctreeQueryPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleOctreeFileReplacement(QSharedPointer message); - void handleOctreeFileReplacementFromURL(QSharedPointer message); + //void handleOctreeFileReplacement(QSharedPointer message); + //void handleOctreeFileReplacementFromURL(QSharedPointer message); + void handleOctreeDataFileReply(QSharedPointer message); void removeSendThread(); protected: @@ -159,11 +172,13 @@ protected: QString getFileLoadTime(); QString getConfiguration(); QString getStatusLink(); + + void beginRunning(QByteArray replaceData); UniqueSendThread createSendThread(const SharedNodePointer& node); virtual UniqueSendThread newSendThread(const SharedNodePointer& node); - void replaceContentFromMessageData(QByteArray content); + //void replaceContentFromMessageData(QByteArray content); int _argc; const char** _argv; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index b4a6b3af93..60cb1e349b 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -178,7 +178,7 @@ void EntityScriptServer::updateEntityPPS() { int numRunningScripts = _entitiesScriptEngine->getNumRunningEntityScripts(); int pps; if (std::numeric_limits::max() / _entityPPSPerScript < numRunningScripts) { - qWarning() << QString("Integer multiplaction would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript); + qWarning() << QString("Integer multiplication would overflow, clamping to maxint: %1 * %2").arg(numRunningScripts).arg(_entityPPSPerScript); pps = std::numeric_limits::max(); pps = std::min(_maxEntityPPS, pps); } else { diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index c1e275e4d3..0e958b9537 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -22,7 +22,17 @@ setup_memory_debugger() symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources") # link the shared hifi libraries -link_hifi_libraries(embedded-webserver networking shared avatars) +link_hifi_libraries(embedded-webserver networking shared avatars octree) + +add_dependency_external_projects(quazip) + +find_package(QuaZip REQUIRED) +target_include_directories(${TARGET_NAME} SYSTEM PUBLIC ${QUAZIP_INCLUDE_DIRS}) +target_link_libraries(${TARGET_NAME} ${QUAZIP_LIBRARIES}) + +if (WIN32) + add_paths_to_fixup_libs(${QUAZIP_DLL_PATH}) +endif () # find OpenSSL find_package(OpenSSL REQUIRED) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp new file mode 100644 index 0000000000..0eca10f8af --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -0,0 +1,303 @@ +// +// DomainContentBackupManager.cpp +// libraries/octree/src +// +// Created by Brad Hefta-Gaub on 8/21/13. +// Copyright 2013 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 +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "DomainServer.h" +#include "DomainContentBackupManager.h" +const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds + +// Backup format looks like: daily_backup-TIMESTAMP.zip +const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; +const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); + +void DomainContentBackupManager::addCreateBackupHandler(CreateBackupHandler handler) { + _backupHandlers.push_back(handler); +} + +DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory, + const QJsonObject& settings, + int persistInterval, + bool debugTimestampNow) + : _backupDirectory(backupDirectory), + _persistInterval(persistInterval), + _initialLoadComplete(false), + _loadTimeUSecs(0), + _lastCheck(0), + _debugTimestampNow(debugTimestampNow), + _lastTimeDebug(0) { + parseSettings(settings); +} + +void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { + qDebug() << settings << settings["backups"] << settings["backups"].isArray(); + if (settings["backups"].isArray()) { + const QJsonArray& backupRules = settings["backups"].toArray(); + qCDebug(domain_server) << "BACKUP RULES:"; + + for (const QJsonValue& value : backupRules) { + QJsonObject obj = value.toObject(); + + int interval = 0; + int count = 0; + + QJsonValue intervalVal = obj["backupInterval"]; + if (intervalVal.isString()) { + interval = intervalVal.toString().toInt(); + } else { + interval = intervalVal.toInt(); + } + + QJsonValue countVal = obj["maxBackupVersions"]; + if (countVal.isString()) { + count = countVal.toString().toInt(); + } else { + count = countVal.toInt(); + } + + auto name = obj["Name"].toString(); + auto format = obj["format"].toString(); + format = name.replace(" ", "_").toLower() + "-"; + + qCDebug(domain_server) << " Name:" << name; + qCDebug(domain_server) << " format:" << format; + qCDebug(domain_server) << " interval:" << interval; + qCDebug(domain_server) << " count:" << count; + + BackupRule newRule = { name, interval, format, count, 0 }; + + newRule.lastBackupSeconds = getMostRecentBackupTimeInSecs(format); + + if (newRule.lastBackupSeconds > 0) { + auto now = QDateTime::currentSecsSinceEpoch(); + auto sinceLastBackup = now - newRule.lastBackupSeconds; + qCDebug(domain_server).noquote() << " lastBackup:" << formatSecTime(sinceLastBackup) << "ago"; + } else { + qCDebug(domain_server) << " lastBackup: NEVER"; + } + + _backupRules << newRule; + } + } else { + qCDebug(domain_server) << "BACKUP RULES: NONE"; + } +} + +int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& format) { + int64_t mostRecentBackupInSecs = 0; + + QString mostRecentBackupFileName; + QDateTime mostRecentBackupTime; + + bool recentBackup = getMostRecentBackup(format, mostRecentBackupFileName, mostRecentBackupTime); + + if (recentBackup) { + mostRecentBackupInSecs = mostRecentBackupTime.toSecsSinceEpoch(); + } + + return mostRecentBackupInSecs; +} + +bool DomainContentBackupManager::process() { + if (isStillRunning()) { + constexpr int64_t MSECS_TO_USECS = 1000; + constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms + std::this_thread::sleep_for(std::chrono::microseconds(USECS_TO_SLEEP)); + + int64_t now = usecTimestampNow(); + int64_t sinceLastSave = now - _lastCheck; + int64_t intervalToCheck = _persistInterval * MSECS_TO_USECS; + + if (sinceLastSave > intervalToCheck) { + _lastCheck = now; + persist(); + } + } + + // if we were asked to debugTimestampNow do that now... + if (_debugTimestampNow) { + + quint64 now = usecTimestampNow(); + quint64 sinceLastDebug = now - _lastTimeDebug; + quint64 DEBUG_TIMESTAMP_INTERVAL = 600000000; // every 10 minutes + + if (sinceLastDebug > DEBUG_TIMESTAMP_INTERVAL) { + _lastTimeDebug = usecTimestampNow(true); // ask for debug output + } + } + + return isStillRunning(); +} + +void DomainContentBackupManager::aboutToFinish() { + qCDebug(domain_server) << "Persist thread about to finish..."; + persist(); +} + +void DomainContentBackupManager::persist() { + QDir backupDir { _backupDirectory }; + backupDir.mkpath("."); + + // create our "lock" file to indicate we're saving. + QString lockFileName = _backupDirectory + "/running.lock"; + + std::ofstream lockFile(qPrintable(lockFileName), std::ios::out | std::ios::binary); + if (lockFile.is_open()) { + backup(); + + lockFile.close(); + remove(qPrintable(lockFileName)); + } +} + +bool DomainContentBackupManager::getMostRecentBackup(const QString& format, + QString& mostRecentBackupFileName, + QDateTime& mostRecentBackupTime) { + QRegExp formatRE { QRegExp::escape(format) + "(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + + QStringList filters; + filters << format + "*.zip"; + + bool bestBackupFound = false; + QString bestBackupFile; + QDateTime bestBackupFileTime; + + // Iterate over all of the backup files in the persist location + QDirIterator dirIterator(_backupDirectory, filters, QDir::Files | QDir::NoSymLinks, QDirIterator::NoIteratorFlags); + while (dirIterator.hasNext()) { + dirIterator.next(); + auto fileName = dirIterator.fileInfo().fileName(); + + if (formatRE.exactMatch(fileName)) { + auto datetime = formatRE.cap(1); + auto createdAt = QDateTime::fromString(datetime, DATETIME_FORMAT); + + if (!createdAt.isValid()) { + qDebug() << "Skipping backup with invalid timestamp: " << datetime; + continue; + } + + qDebug() << "Checking " << dirIterator.fileInfo().filePath(); + + // Based on last modified date, track the most recently modified file as the best backup + if (createdAt > bestBackupFileTime) { + bestBackupFound = true; + bestBackupFile = dirIterator.filePath(); + bestBackupFileTime = createdAt; + } + } else { + qDebug() << "NO match: " << fileName << formatRE; + } + } + + // If we found a backup then return the results + if (bestBackupFound) { + mostRecentBackupFileName = bestBackupFile; + mostRecentBackupTime = bestBackupFileTime; + } + return bestBackupFound; +} + +void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { + QDir backupDir { _backupDirectory }; + if (backupDir.exists() && rule.maxBackupVersions > 0) { + qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name << "..."; + + auto matchingFiles = + backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + + int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + for (int i = 0; i < backupsToDelete; ++i) { + auto fileInfo = matchingFiles[i].absoluteFilePath(); + QFile backupFile(fileInfo); + if (backupFile.remove()) { + qCDebug(domain_server) << "Removed old backup: " << backupFile.fileName(); + } else { + qCDebug(domain_server) << "Failed to remove old backup: " << backupFile.fileName(); + } + } + + qCDebug(domain_server) << "Done rolling old backup versions..."; + } else { + qCDebug(domain_server) << "Rolling backups for rule" << rule.name << "." + << " Max Rolled Backup Versions less than 1 [" << rule.maxBackupVersions << "]." + << " No need to roll backups..."; + } +} + +void DomainContentBackupManager::backup() { + auto nowDateTime = QDateTime::currentDateTime(); + auto nowSeconds = nowDateTime.toSecsSinceEpoch(); + + for (BackupRule& rule : _backupRules) { + auto secondsSinceLastBackup = nowSeconds - rule.lastBackupSeconds; + + qCDebug(domain_server) << "Checking [" << rule.name << "] - Time since last backup [" << secondsSinceLastBackup + << "] " + << "compared to backup interval [" << rule.intervalSeconds << "]..."; + + if (secondsSinceLastBackup > rule.intervalSeconds) { + qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name + << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; + + auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; + auto zip = new QuaZip(_backupDirectory + "/" + fileName); + zip->open(QuaZip::mdAdd); + + for (auto& handler : _backupHandlers) { + handler(zip); + } + + zip->close(); + + qDebug() << "Created backup: " << fileName; + + removeOldBackupVersions(rule); + + if (rule.maxBackupVersions > 0) { + // Execute backup + auto result = true; + if (result) { + qCDebug(domain_server) << "DONE backing up persist file..."; + rule.lastBackupSeconds = nowSeconds; + } else { + qCDebug(domain_server) << "ERROR in backing up persist file..."; + perror("ERROR in backing up persist file"); + } + } else { + qCDebug(domain_server) << "This backup rule" << rule.name << " has Max Rolled Backup Versions less than 1 [" + << rule.maxBackupVersions << "]." + << " There are no backups to be done..."; + } + } else { + qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]..."; + } + } +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h new file mode 100644 index 0000000000..20408fe486 --- /dev/null +++ b/domain-server/src/DomainContentBackupManager.h @@ -0,0 +1,88 @@ +// +// DomainContentBackupManager.h +// libraries/octree/src +// +// Created by Brad Hefta-Gaub on 8/21/13. +// Copyright 2013 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 +// + +#ifndef hifi_DomainContentBackupManager_h +#define hifi_DomainContentBackupManager_h + +#include +#include +#include + +#include +#include + +#include + +using BackupResult = std::vector; +using CreateBackupHandler = std::function; +using RecoverBackupHandler = std::function; + +class DomainContentBackupManager : public GenericThread { + Q_OBJECT +public: + class BackupRule { + public: + QString name; + int intervalSeconds; + QString extensionFormat; + int maxBackupVersions; + qint64 lastBackupSeconds; + }; + + static const int DEFAULT_PERSIST_INTERVAL; + + DomainContentBackupManager(const QString& rootBackupDirectory, + const QJsonObject& settings, + int persistInterval = DEFAULT_PERSIST_INTERVAL, + bool debugTimestampNow = false); + + void addCreateBackupHandler(CreateBackupHandler handler); + bool isInitialLoadComplete() const { return _initialLoadComplete; } + int64_t getLoadElapsedTime() const { return _loadTimeUSecs; } + + void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist + + void replaceData(QByteArray data); + +signals: + void loadCompleted(); + +protected: + /// Implements generic processing behavior for this thread. + bool process() override; + + void persist(); + void backup(); + void removeOldBackupVersions(const BackupRule& rule); + bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); + int64_t getMostRecentBackupTimeInSecs(const QString& format); + void parseSettings(const QJsonObject& settings); + +private: + QString _backupDirectory; + std::vector _backupHandlers; + int _persistInterval; + bool _initialLoadComplete; + + int64_t _loadTimeUSecs; + + time_t _lastPersistTime; + int64_t _lastCheck; + bool _wantBackup{ true }; + QVector _backupRules; + + bool _debugTimestampNow; + int64_t _lastTimeDebug; +}; + +#endif // hifi_DomainContentBackupManager_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 68a36195d9..e083710d35 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -47,7 +48,14 @@ #include "DomainServerNodeData.h" #include "NodeConnectionData.h" +#include + +#include + +Q_LOGGING_CATEGORY(domain_server, "hifi.domain_server") + const QString ACCESS_TOKEN_KEY_PATH = "metaverse.access_token"; +const QString DomainServer::REPLACEMENT_FILE_EXTENSION = ".replace"; int const DomainServer::EXIT_CODE_REBOOT = 234923; @@ -280,6 +288,30 @@ DomainServer::DomainServer(int argc, char* argv[]) : qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet; } } + + qDebug() << "Starting persist thread"; + if (QDir(getEntitiesDirPath()).mkpath(".")) { + qCDebug(domain_server) << "Created entities data directory"; + } + maybeHandleReplacementEntityFile(); + auto entitiesFilePath = getEntitiesFilePath(); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); + _contentManager->addCreateBackupHandler([entitiesFilePath](QuaZip* zip) { + qDebug() << "Creating a backup from handler"; + + QFile entitiesFile { entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "Failed to write entities file to backup:" << zipFile.getZipError(); + } + } + }); + _contentManager->initialize(true); } void DomainServer::parseCommandLine() { @@ -352,6 +384,11 @@ DomainServer::~DomainServer() { // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); + + if (_contentManager) { + _contentManager->aboutToFinish(); + _contentManager->terminating(); + } } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { @@ -691,6 +728,12 @@ void DomainServer::setupNodeListAndAssignments() { packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket"); packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK"); + packetReceiver.registerListener(PacketType::OctreeDataFileRequest, this, "processOctreeDataRequestMessage"); + packetReceiver.registerListener(PacketType::OctreeDataPersist, this, "processOctreeDataPersistMessage"); + + packetReceiver.registerListener(PacketType::OctreeFileReplacement, this, "handleOctreeFileReplacementRequest"); + packetReceiver.registerListener(PacketType::OctreeFileReplacementFromUrl, this, "handleOctreeFileReplacementFromURLRequest"); + // set a custom packetVersionMatch as the verify packet operator for the udt::Socket nodeList->setPacketFilterOperator(&DomainServer::isPacketVerified); @@ -1605,6 +1648,7 @@ void DomainServer::sendHeartbeatToIceServer() { qWarning() << "Waiting for keypair generation to complete before sending ICE heartbeat."; if (!limitedNodeList->getSessionUUID().isNull()) { + qDebug() << "generating keypair"; accountManager->generateNewDomainKeypair(limitedNodeList->getSessionUUID()); } else { qWarning() << "Attempting to send ICE server heartbeat with no domain ID. This is not supported"; @@ -1695,10 +1739,88 @@ void DomainServer::sendHeartbeatToIceServer() { } else { qDebug() << "Not sending ice-server heartbeat since there is no selected ice-server."; qDebug() << "Waiting for" << _iceServerAddr << "host lookup response"; - } } +void DomainServer::processOctreeDataPersistMessage(QSharedPointer message) { + qDebug() << "Received octree data persist message"; + auto data = message->readAll(); + auto filePath = getEntitiesFilePath(); + + QFile f(filePath); + if (f.open(QIODevice::WriteOnly)) { + f.write(data); + OctreeUtils::RawOctreeData octreeData; + if (OctreeUtils::readOctreeDataInfoFromData(data, &octreeData)) { + qCDebug(domain_server) << "Wrote new entiteis file" << octreeData.id << octreeData.version; + } else { + qCDebug(domain_server) << "Failed to read new octree data info"; + } + } else { + qCDebug(domain_server) << "Failed to write new entities file"; + } +} + +QString DomainServer::getContentBackupDir() { + return PathUtils::getAppDataFilePath("backup"); +} + +QString DomainServer::getEntitiesDirPath() { + return PathUtils::getAppDataFilePath("entities"); +} + +QString DomainServer::getEntitiesFilePath() { + return PathUtils::getAppDataFilePath("entities/models.json.gz"); +} + +QString DomainServer::getEntitiesReplacementFilePath() { + return getEntitiesFilePath().append(REPLACEMENT_FILE_EXTENSION); +} + +void DomainServer::processOctreeDataRequestMessage(QSharedPointer message) { + qDebug() << "Got request for octree data from " << message->getSenderSockAddr(); + + bool remoteHasExistingData { false }; + QUuid id; + int version; + message->readPrimitive(&remoteHasExistingData); + if (remoteHasExistingData) { + auto idData = message->read(16); + id = QUuid::fromRfc4122(idData); + message->readPrimitive(&version); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + } else { + qCDebug(domain_server) << "Entity server does not have existing data"; + } + auto entityFilePath = getEntitiesFilePath(); + + //QFile file(entityFilePath); + auto reply = NLPacketList::create(PacketType::OctreeDataFileReply, QByteArray(), true, true); + OctreeUtils::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromFile(entityFilePath, &data)) { + if (data.id == id && data.version <= version) { + qCDebug(domain_server) << "ES has sufficient octree data, not sending data"; + reply->writePrimitive(false); + } else { + qCDebug(domain_server) << "Sending newer octree data to ES"; + QFile file(entityFilePath); + if (file.open(QIODevice::ReadOnly)) { + reply->writePrimitive(true); + reply->write(file.readAll()); + } else { + qCDebug(domain_server) << "Unable to load entity file"; + reply->writePrimitive(false); + } + } + } else { + qCDebug(domain_server) << "Domain server does not have valid octree data"; + reply->writePrimitive(false); + } + + auto nodeList = DependencyManager::get(); + nodeList->sendPacketList(std::move(reply), message->getSenderSockAddr()); +} + void DomainServer::processNodeJSONStatsPacket(QSharedPointer packetList, SharedNodePointer sendingNode) { auto nodeData = static_cast(sendingNode->getLinkedData()); if (nodeData) { @@ -3105,9 +3227,64 @@ void DomainServer::setupGroupCacheRefresh() { } } +void DomainServer::maybeHandleReplacementEntityFile() { + QFile replacementFile(getEntitiesReplacementFilePath()); + if (replacementFile.exists()) { + qCDebug(domain_server) << "Replacing existing entity date with replacement file"; + QFile currentFile(getEntitiesFilePath()); + if (currentFile.exists()) { + if (currentFile.remove()) { + qCDebug(domain_server) << "Removed existing entity file"; + } else { + qCWarning(domain_server) << "Failled to remove existing entity file"; + } + } + if (replacementFile.rename(getEntitiesFilePath())) { + qCDebug(domain_server) << "Successfully updated entities data file with replacement file"; + } else { + qCWarning(domain_server) << "Failed to update entities data file with replacement file"; + } + } +} + void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { // enumerate the nodes and find any octree type servers with active sockets + //Assume we have compressed data + auto compressedOctree = octreeFile; + QByteArray jsonOctree; + + bool wasCompressed = gunzip(compressedOctree, jsonOctree); + if (!wasCompressed) { + // the source was not compressed, assume we were sent regular JSON data + jsonOctree = compressedOctree; + } + + OctreeUtils::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromData(jsonOctree, &data)) { + data.id = QUuid::createUuid(); + data.version = 0; + + gzip(data.toByteArray(), compressedOctree); + + // write the compressed octree data to a special file + auto replacementFilePath = getEntitiesReplacementFilePath(); + QFile replacementFile(replacementFilePath); + if (replacementFile.open(QIODevice::WriteOnly) && replacementFile.write(compressedOctree) != -1) { + // we've now written our replacement file, time to take the server down so it can + // process it when it comes back up + qInfo() << "Wrote octree replacement file to" << replacementFilePath << "- stopping server"; + + QMetaObject::invokeMethod(this, "restart", Qt::QueuedConnection); + } else { + qWarning() << "Could not write replacement octree data to file - refusing to process"; + } + } else { + qDebug() << "Received replacement octree file that is invalid - refusing to process"; + } + + + return; auto limitedNodeList = DependencyManager::get(); limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { return node->getType() == NodeType::EntityServer && node->getActiveSocket(); @@ -3121,3 +3298,37 @@ void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); }); } + +void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer message) { + qInfo() << "Received request to replace content from a url"; + auto node = DependencyManager::get()->findNodeWithAddr(message->getSenderSockAddr()); + if (node) { + qDebug() << "Found node: " << node->getCanReplaceContent(); + } + if (node->getCanReplaceContent()) { + // Convert message data into our URL + QString url(message->getMessage()); + QUrl modelsURL = QUrl(url, QUrl::StrictMode); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest request(modelsURL); + QNetworkReply* reply = networkAccessManager.get(request); + connect(reply, &QNetworkReply::finished, [this, reply, modelsURL]() { + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + handleOctreeFileReplacement(reply->readAll()); + } else { + qDebug() << "Error downloading JSON from specified file: " << modelsURL; + } + }); + } +} + + + + +void DomainServer::handleOctreeFileReplacementRequest(QSharedPointer message) { + auto node = DependencyManager::get()->nodeWithUUID(message->getSourceID()); + if (node->getCanReplaceContent()) { + handleOctreeFileReplacement(message->readAll()); + } +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index c7d779b394..ee0350665e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -32,9 +32,14 @@ #include "DomainServerSettingsManager.h" #include "DomainServerWebSessionData.h" #include "WalletTransaction.h" +#include "DomainContentBackupManager.h" #include "PendingAssignedNodeData.h" +#include + +Q_DECLARE_LOGGING_CATEGORY(domain_server) + typedef QSharedPointer SharedAssignmentPointer; typedef QMultiHash TransactionHash; @@ -65,6 +70,8 @@ public: bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override; bool handleHTTPSRequest(HTTPSConnection* connection, const QUrl& url, bool skipSubHandler = false) override; + static const QString REPLACEMENT_FILE_EXTENSION; + public slots: /// Called by NodeList to inform us a node has been added void nodeAdded(SharedNodePointer node); @@ -84,6 +91,13 @@ private slots: void processICEServerHeartbeatDenialPacket(QSharedPointer message); void processICEServerHeartbeatACK(QSharedPointer message); + void handleOctreeFileReplacementFromURLRequest(QSharedPointer message); + void handleOctreeFileReplacementRequest(QSharedPointer message); + void handleOctreeFileReplacement(QByteArray octreeFile); + + void processOctreeDataRequestMessage(QSharedPointer message); + void processOctreeDataPersistMessage(QSharedPointer message); + void setupPendingAssignmentCredits(); void sendPendingTransactionsToServer(); @@ -91,8 +105,7 @@ private slots: void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); - void handleConnectedNode(SharedNodePointer newNode); - + void handleConnectedNode(SharedNodePointer newNode); void handleTempDomainSuccess(QNetworkReply& requestReply); void handleTempDomainError(QNetworkReply& requestReply); @@ -109,8 +122,6 @@ private slots: void handleSuccessfulICEServerAddressUpdate(QNetworkReply& requestReply); void handleFailedICEServerAddressUpdate(QNetworkReply& requestReply); - void handleOctreeFileReplacement(QByteArray octreeFile); - void updateReplicatedNodes(); void updateDownstreamNodes(); void updateUpstreamNodes(); @@ -127,6 +138,13 @@ private: const QUuid& getID(); void parseCommandLine(); + QString getContentBackupDir(); + QString getEntitiesDirPath(); + QString getEntitiesFilePath(); + QString getEntitiesReplacementFilePath(); + + void maybeHandleReplacementEntityFile(); + void setupNodeListAndAssignments(); bool optionallySetupOAuth(); bool optionallyReadX509KeyAndCertificate(); @@ -252,6 +270,8 @@ private: bool _sendICEServerAddressToMetaverseAPIInProgress { false }; bool _sendICEServerAddressToMetaverseAPIRedo { false }; + std::unique_ptr _contentManager { nullptr }; + QHash> _pendingOAuthConnections; QThread _assetClientThread; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5a340f471e..c031c0e8d4 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6240,13 +6240,15 @@ bool Application::askToReplaceDomainContent(const QString& url) { // Given confirmation, send request to domain server to replace content qCDebug(interfaceapp) << "Attempting to replace domain content: " << url; QByteArray urlData(url.toUtf8()); - auto limitedNodeList = DependencyManager::get(); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { return node->getType() == NodeType::EntityServer && node->getActiveSocket(); - }, [&urlData, limitedNodeList](const SharedNodePointer& octreeNode) { + }, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) { auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), *octreeNode); + qDebug() << "WRiting url data: " << urlData; + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); }); auto addressManager = DependencyManager::get(); addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); diff --git a/interface/src/ui/DomainConnectionModel.cpp b/interface/src/ui/DomainConnectionModel.cpp index b9e4c1348e..83aa18420c 100644 --- a/interface/src/ui/DomainConnectionModel.cpp +++ b/interface/src/ui/DomainConnectionModel.cpp @@ -98,4 +98,4 @@ void DomainConnectionModel::refresh() { //inform view that we want refresh data beginResetModel(); endResetModel(); -} \ No newline at end of file +} diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 60bcc85575..4f96a6d072 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2244,6 +2244,8 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer if (! entityDescription.contains("Entities")) { entityDescription["Entities"] = QVariantList(); } + entityDescription["DataVersion"] = ++_persistDataVersion; + entityDescription["Id"] = _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); @@ -2256,6 +2258,14 @@ bool EntityTree::readFromMap(QVariantMap& map) { int contentVersion = map["Version"].toInt(); bool needsConversion = (contentVersion < (int)EntityVersion::ZoneLightInheritModes); + if (map.contains("Id")) { + _persistID = map["Id"].toUuid(); + } + + if (map.contains("DataVersion")) { + _persistDataVersion = map["DataVersion"].toInt(); + } + // map will have a top-level list keyed as "Entities". This will be extracted // and iterated over. Each member of this list is converted to a QVariantMap, then // to a QScriptValue, and then to EntityItemProperties. These properties are used diff --git a/libraries/image/CMakeLists.txt b/libraries/image/CMakeLists.txt index 442fa714b3..e6a1856327 100644 --- a/libraries/image/CMakeLists.txt +++ b/libraries/image/CMakeLists.txt @@ -5,7 +5,8 @@ link_hifi_libraries(shared gpu) if (NOT ANDROID) add_dependency_external_projects(nvtt) find_package(NVTT REQUIRED) + target_include_directories(${TARGET_NAME} PRIVATE ${NVTT_INCLUDE_DIRS}) target_link_libraries(${TARGET_NAME} ${NVTT_LIBRARIES}) add_paths_to_fixup_libs(${NVTT_DLL_PATH}) -endif() \ No newline at end of file +endif() diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 2343695914..3516fe948a 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -326,6 +326,7 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe static QMultiMap hashDebugSuppressMap; if (!hashDebugSuppressMap.contains(sourceID, headerType)) { + qCDebug(networking) << packetHeaderHash << expectedHash; qCDebug(networking) << "Packet hash mismatch on" << headerType << "- Sender" << sourceID; hashDebugSuppressMap.insert(sourceID, headerType); diff --git a/libraries/networking/src/ThreadedAssignment.h b/libraries/networking/src/ThreadedAssignment.h index 8b35acaac5..007e41a543 100644 --- a/libraries/networking/src/ThreadedAssignment.h +++ b/libraries/networking/src/ThreadedAssignment.h @@ -18,8 +18,6 @@ #include "Assignment.h" -using DownstreamNodeFoundCallback = std::function; - class ThreadedAssignment : public Assignment { Q_OBJECT public: @@ -47,10 +45,10 @@ protected: QTimer _domainServerTimer; QTimer _statsTimer; int _numQueuedCheckIns { 0 }; - + protected slots: void domainSettingsRequestFailed(); - + private slots: void checkInWithDomainServerOrExit(); }; diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 5757cea496..7cd02608a1 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -126,6 +126,11 @@ public: EntityScriptCallMethod, ChallengeOwnershipRequest, ChallengeOwnershipReply, + + OctreeDataFileRequest, + OctreeDataFileReply, + OctreeDataPersist, + NUM_PACKET_TYPE }; @@ -165,6 +170,8 @@ public: << PacketTypeEnum::Value::DomainConnectionDenied << PacketTypeEnum::Value::DomainServerPathQuery << PacketTypeEnum::Value::DomainServerPathResponse << PacketTypeEnum::Value::DomainServerAddedNode << PacketTypeEnum::Value::DomainServerConnectionToken << PacketTypeEnum::Value::DomainSettingsRequest + << PacketTypeEnum::Value::OctreeDataFileRequest << PacketTypeEnum::Value::OctreeDataFileReply + << PacketTypeEnum::Value::OctreeDataPersist << PacketTypeEnum::Value::OctreeFileReplacementFromUrl << PacketTypeEnum::Value::DomainSettings << PacketTypeEnum::Value::ICEServerPeerInformation << PacketTypeEnum::Value::ICEServerQuery << PacketTypeEnum::Value::ICEServerHeartbeat << PacketTypeEnum::Value::ICEServerHeartbeatACK << PacketTypeEnum::Value::ICEPing diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 55643985c8..019ae07c16 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -328,7 +328,7 @@ void Socket::checkForReadyReadBackup() { void Socket::readPendingDatagrams() { int packetSizeWithHeader = -1; - while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) != -1) { + while ((packetSizeWithHeader = _udpSocket.pendingDatagramSize()) > 0) { // we're reading a packet so re-start the readyRead backup timer _readyReadBackupTimer->start(); @@ -517,7 +517,7 @@ void Socket::handleSocketError(QAbstractSocket::SocketError socketError) { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(SOCKET_REGEX); - qCDebug(networking) << "udt::Socket error - " << socketError; + qCDebug(networking) << "udt::Socket error - " << socketError << _udpSocket.errorString(); } void Socket::handleStateChanged(QAbstractSocket::SocketState socketState) { diff --git a/libraries/octree/CMakeLists.txt b/libraries/octree/CMakeLists.txt index bea036add3..228779dbba 100644 --- a/libraries/octree/CMakeLists.txt +++ b/libraries/octree/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME octree) +include_directories(system /usr/local/Cellar/qt5/5.9.1/include) setup_hifi_library() link_hifi_libraries(shared networking) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index c63ff2f560..23352a548c 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1757,6 +1757,19 @@ bool Octree::readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); + /* + if (success) { + if (asMap.contains("DataVersion") && asMap.contains("Id")) { + bool versionOk; + auto dataVersion = asMap["DataVersion"].toLongLong(&versionOk); + if (versionOk) { + auto id = asMap["Id"].toUuid(); + _persistDataVersion = dataVersion; + _persistID = id; + } + } + } + */ delete[] rawData; return success; } @@ -1778,11 +1791,9 @@ bool Octree::writeToFile(const char* fileName, const OctreeElementPointer& eleme return success; } -bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& element, bool doGzip) { +bool Octree::toJSON(QJsonDocument* doc, const OctreeElementPointer& element) { QVariantMap entityDescription; - qCDebug(octree, "Saving JSON SVO to file %s...", fileName); - OctreeElementPointer top; if (element) { top = element; @@ -1802,17 +1813,35 @@ bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& e return false; } - // convert the QVariantMap to JSON - QByteArray jsonData = QJsonDocument::fromVariant(entityDescription).toJson(); - QByteArray jsonDataForFile; + *doc = QJsonDocument::fromVariant(entityDescription); + return true; +} - if (doGzip) { - if (!gzip(jsonData, jsonDataForFile, -1)) { - qCritical("unable to gzip data while saving to json."); - return false; - } +bool Octree::toGzippedJSON(QByteArray* data, const OctreeElementPointer& element) { + QJsonDocument doc; + if (!toJSON(&doc, element)) { + qCritical("Failed to convert Entities to QVariantMap while converting to json."); + return false; + } + + QByteArray jsonData = doc.toJson(); + + if (!gzip(jsonData, *data, -1)) { + qCritical("Unable to gzip data while saving to json."); + return false; } else { - jsonDataForFile = jsonData; + qDebug() <<"Did gzip!"; + } + + return true; +} + +bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& element, bool doGzip) { + qCDebug(octree, "Saving JSON SVO to file %s...", fileName); + + QByteArray jsonDataForFile; + if (!toGzippedJSON(&jsonDataForFile)) { + return false; } QFile persistFile(fileName); @@ -1823,6 +1852,7 @@ bool Octree::writeToJSONFile(const char* fileName, const OctreeElementPointer& e qCritical("Could not write to JSON description of entities."); } + return success; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 1648cb0f47..1b9495717b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -283,8 +283,10 @@ public: void loadOctreeFile(const char* fileName); // Octree exporters - bool writeToFile(const char* filename, const OctreeElementPointer& element = NULL, QString persistAsFileType = "json.gz"); - bool writeToJSONFile(const char* filename, const OctreeElementPointer& element = NULL, bool doGzip = false); + bool toJSON(QJsonDocument* doc, const OctreeElementPointer& element = nullptr); + bool toGzippedJSON(QByteArray* data, const OctreeElementPointer& element = nullptr); + bool writeToFile(const char* filename, const OctreeElementPointer& element = nullptr, QString persistAsFileType = "json.gz"); + bool writeToJSONFile(const char* filename, const OctreeElementPointer& element = nullptr, bool doGzip = false); virtual bool writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues, bool skipThoseWithBadParents) = 0; @@ -326,6 +328,11 @@ public: virtual void dumpTree() { } virtual void pruneTree() { } + void setEntityVersionInfo(QUuid id, int64_t dataVersion) { + _persistID = id; + _persistDataVersion = dataVersion; + } + virtual void resetEditStats() { } virtual quint64 getAverageDecodeTime() const { return 0; } virtual quint64 getAverageLookupTime() const { return 0; } @@ -359,6 +366,9 @@ protected: OctreeElementPointer _rootElement = nullptr; + QUuid _persistID { QUuid::createUuid() }; + int _persistDataVersion { 0 }; + bool _isDirty; bool _shouldReaverage; bool _stopImport; diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index ea6bd28fc4..9c9a4d40db 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -31,18 +31,19 @@ #include "OctreeLogging.h" #include "OctreePersistThread.h" +#include "OctreeUtils.h" const int OctreePersistThread::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds -const QString OctreePersistThread::REPLACEMENT_FILE_EXTENSION = ".replace"; OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval, bool wantBackup, const QJsonObject& settings, bool debugTimestampNow, - QString persistAsFileType) : + QString persistAsFileType, const QByteArray& replacementData) : _tree(tree), _filename(filename), _backupDirectory(backupDirectory), _persistInterval(persistInterval), _initialLoadComplete(false), + _replacementData(replacementData), _loadTimeUSecs(0), _lastCheck(0), _wantBackup(wantBackup), @@ -52,6 +53,7 @@ OctreePersistThread::OctreePersistThread(OctreePointer tree, const QString& file { parseSettings(settings); + // in case the persist filename has an extension that doesn't match the file type QString sansExt = fileNameWithoutExtension(_filename, PERSIST_EXTENSIONS); _filename = sansExt + "." + _persistAsFileType; @@ -132,51 +134,56 @@ quint64 OctreePersistThread::getMostRecentBackupTimeInUsecs(const QString& forma return mostRecentBackupInUsecs; } -void OctreePersistThread::possiblyReplaceContent() { - // before we load the normal file, check if there's a pending replacement file - auto replacementFileName = _filename + REPLACEMENT_FILE_EXTENSION; +void OctreePersistThread::replaceData(QByteArray data) { + backupCurrentFile(); - QFile replacementFile { replacementFileName }; - if (replacementFile.exists()) { - // we have a replacement file to process - qDebug() << "Replacing models file with" << replacementFileName; - - // first take the current models file and move it to a different filename, appended with the timestamp - QFile currentFile { _filename }; - if (currentFile.exists()) { - static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; - auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); - - if (currentFile.rename(backupFileName)) { - qDebug() << "Moved previous models file to" << backupFileName; - } else { - qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; - - if (!replacementFile.remove()) { - qWarning() << "Could not remove replacement models file from" << replacementFileName - << "- replacement will be re-attempted on next server restart"; - return; - } - } - } - - // rename the replacement file to match what the persist thread is just about to read - if (!replacementFile.rename(_filename)) { - qWarning() << "Could not replace models file with" << replacementFileName << "- starting with empty models file"; - } + QFile currentFile { _filename }; + if (currentFile.open(QIODevice::WriteOnly)) { + currentFile.write(data); + qDebug() << "Wrote replacement data"; + } else { + qWarning() << "Failed to write replacement data"; } } +// Return true if current file is backed up successfully or doesn't exist. +bool OctreePersistThread::backupCurrentFile() { + // first take the current models file and move it to a different filename, appended with the timestamp + QFile currentFile { _filename }; + if (currentFile.exists()) { + static const QString FILENAME_TIMESTAMP_FORMAT = "yyyyMMdd-hhmmss"; + auto backupFileName = _filename + ".backup." + QDateTime::currentDateTime().toString(FILENAME_TIMESTAMP_FORMAT); + + if (currentFile.rename(backupFileName)) { + qDebug() << "Moved previous models file to" << backupFileName; + return true; + } else { + qWarning() << "Could not backup previous models file to" << backupFileName << "- removing replacement models file"; + return false; + } + } + return true; +} bool OctreePersistThread::process() { if (!_initialLoadComplete) { - possiblyReplaceContent(); - quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; - bool persistantFileRead; + if (_replacementData.isNull()) { + sendLatestEntityDataToDS(); + } else { + replaceData(_replacementData); + _replacementData.clear(); + } + + OctreeUtils::RawOctreeData data; + if (OctreeUtils::readOctreeDataInfoFromFile(_filename, &data)) { + _tree->setEntityVersionInfo(data.id, data.version); + } + + bool persistentFileRead; _tree->withWriteLock([&] { PerformanceWarning warn(true, "Loading Octree File", true); @@ -199,7 +206,7 @@ bool OctreePersistThread::process() { qCDebug(octree) << "Loading Octree... lock file removed:" << lockFileName; } - persistantFileRead = _tree->readFromFile(qPrintable(_filename.toLocal8Bit())); + persistentFileRead = _tree->readFromFile(qPrintable(_filename.toLocal8Bit())); _tree->pruneTree(); }); @@ -207,7 +214,7 @@ bool OctreePersistThread::process() { _loadTimeUSecs = loadDone - loadStarted; _tree->clearDirtyBit(); // the tree is clean since we just loaded it - qCDebug(octree, "DONE loading Octrees from file... fileRead=%s", debug::valueOf(persistantFileRead)); + qCDebug(octree, "DONE loading Octrees from file... fileRead=%s", debug::valueOf(persistentFileRead)); unsigned long nodeCount = OctreeElement::getNodeCount(); unsigned long internalNodeCount = OctreeElement::getInternalNodeCount(); @@ -272,7 +279,6 @@ bool OctreePersistThread::process() { return isStillRunning(); // keep running till they terminate us } - void OctreePersistThread::aboutToFinish() { qCDebug(octree) << "Persist thread about to finish..."; persist(); @@ -319,6 +325,23 @@ void OctreePersistThread::persist() { remove(qPrintable(lockFileName)); qCDebug(octree) << "saving Octree lock file removed:" << lockFileName; } + + sendLatestEntityDataToDS(); + } +} + +void OctreePersistThread::sendLatestEntityDataToDS() { + qDebug() << "Sending latest entity data to DS"; + auto nodeList = DependencyManager::get(); + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + QByteArray data; + if (_tree->toGzippedJSON(&data)) { + auto message = NLPacketList::create(PacketType::OctreeDataPersist, QByteArray(), true, true); + message->write(data); + nodeList->sendPacketList(std::move(message), domainHandler.getSockAddr()); + } else { + qCWarning(octree) << "Failed to persist octree to DS"; } } @@ -453,7 +476,6 @@ void OctreePersistThread::rollOldBackupVersions(const BackupRule& rule) { } } - void OctreePersistThread::backup() { qCDebug(octree) << "backup operation wantBackup:" << _wantBackup; if (_wantBackup) { diff --git a/libraries/octree/src/OctreePersistThread.h b/libraries/octree/src/OctreePersistThread.h index 2441223467..3fdad2c3f7 100644 --- a/libraries/octree/src/OctreePersistThread.h +++ b/libraries/octree/src/OctreePersistThread.h @@ -18,7 +18,6 @@ #include #include "Octree.h" -/// Generalized threaded processor for handling received inbound packets. class OctreePersistThread : public GenericThread { Q_OBJECT public: @@ -32,11 +31,11 @@ public: }; static const int DEFAULT_PERSIST_INTERVAL; - static const QString REPLACEMENT_FILE_EXTENSION; OctreePersistThread(OctreePointer tree, const QString& filename, const QString& backupDirectory, int persistInterval = DEFAULT_PERSIST_INTERVAL, bool wantBackup = false, - const QJsonObject& settings = QJsonObject(), bool debugTimestampNow = false, QString persistAsFileType="json.gz"); + const QJsonObject& settings = QJsonObject(), bool debugTimestampNow = false, + QString persistAsFileType="json.gz", const QByteArray& replacementData = QByteArray()); bool isInitialLoadComplete() const { return _initialLoadComplete; } quint64 getLoadElapsedTime() const { return _loadTimeUSecs; } @@ -61,7 +60,10 @@ protected: bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); quint64 getMostRecentBackupTimeInUsecs(const QString& format); void parseSettings(const QJsonObject& settings); - void possiblyReplaceContent(); + bool backupCurrentFile(); + + void replaceData(QByteArray data); + void sendLatestEntityDataToDS(); private: OctreePointer _tree; @@ -69,6 +71,7 @@ private: QString _backupDirectory; int _persistInterval; bool _initialLoadComplete; + QByteArray _replacementData; quint64 _loadTimeUSecs; diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index ca15324d4e..d8925a10ca 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -16,7 +16,11 @@ #include #include +#include +#include +#include +#include float calculateRenderAccuracy(const glm::vec3& position, const AABox& bounds, @@ -75,3 +79,76 @@ float getOrthographicAccuracySize(float octreeSizeScale, int boundaryLevelAdjust const float smallestSize = 0.01f; return (smallestSize * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT) / boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale); } + +bool OctreeUtils::readOctreeFile(QString path, QJsonDocument* doc) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Cannot open json file for reading: " << path; + return false; + } + + QByteArray data = file.readAll(); + QByteArray jsonData; + + if (path.endsWith(".json.gz")) { + if (!gunzip(data, jsonData)) { + qCritical() << "json File not in gzip format: " << path; + return false; + } + } else { + jsonData = data; + } + + *doc = QJsonDocument::fromJson(jsonData); + return !doc->isNull(); +} + +bool readOctreeDataInfoFromJSON(QJsonObject root, OctreeUtils::RawOctreeData* octreeData) { + if (root.contains("Id") && root.contains("DataVersion")) { + octreeData->id = root["Id"].toVariant().toUuid(); + octreeData->version = root["DataVersion"].toInt(); + } + if (root.contains("Entities")) { + octreeData->octreeData = root["Entities"].toArray(); + } + return true; +} + +bool OctreeUtils::readOctreeDataInfoFromData(QByteArray data, OctreeUtils::RawOctreeData* octreeData) { + QByteArray jsonData; + if (gunzip(data, jsonData)) { + data = jsonData; + } + + auto doc = QJsonDocument::fromJson(data); + if (doc.isNull()) { + return false; + } + + auto root = doc.object(); + return readOctreeDataInfoFromJSON(root, octreeData); +} + +bool OctreeUtils::readOctreeDataInfoFromFile(QString path, OctreeUtils::RawOctreeData* octreeData) { + QJsonDocument doc; + if (!OctreeUtils::readOctreeFile(path, &doc)) { + return false; + } + + auto root = doc.object(); + return readOctreeDataInfoFromJSON(root, octreeData); +} + +QByteArray OctreeUtils::RawOctreeData::toByteArray() { + QJsonObject obj { + { "DataVersion", QJsonValue((qint64)version) }, + { "Id", QJsonValue(id.toString()) }, + { "Version", QJsonValue(5) }, + { "Entities", octreeData } + }; + + QJsonDocument doc; + doc.setObject(obj); + + return doc.toJson(); +} diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 0f87bb6f68..e5c7b617cd 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -14,7 +14,30 @@ #include "OctreeConstants.h" +#include +#include + class AABox; +class QJsonDocument; + +namespace OctreeUtils { + +// RawOctreeData is an intermediate format between JSON and a fully deserialized Octree. +class RawOctreeData { +public: + QUuid id { QUuid() }; + int64_t version { -1 }; + + QJsonArray octreeData; + + QByteArray toByteArray(); +}; + +bool readOctreeFile(QString path, QJsonDocument* doc); +bool readOctreeDataInfoFromData(QByteArray data, RawOctreeData* octreeData); +bool readOctreeDataInfoFromFile(QString path, RawOctreeData* octreeData); + +} /// renderAccuracy represents a floating point "visibility" of an object based on it's view from the camera. At a simple /// level it returns 0.0f for things that are so small for the current settings that they could not be visible. diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index 8e5c30711c..f46d0768c1 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -105,7 +105,7 @@ void usecTimestampNowForceClockSkew(qint64 clockSkew) { ::usecTimestampNowAdjust = clockSkew; } -static qint64 TIME_REFERENCE = 0; // in usec +static std::atomic TIME_REFERENCE { 0 }; // in usec static std::once_flag usecTimestampNowIsInitialized; static QElapsedTimer timestampTimer; @@ -771,6 +771,10 @@ QString formatUsecTime(double usecs) { return formatUsecTime(usecs); } +QString formatSecTime(qint64 secs) { + return formatUsecTime(secs * 1000000); +} + QString formatSecondsElapsed(float seconds) { QString result; diff --git a/libraries/shared/src/SharedUtil.h b/libraries/shared/src/SharedUtil.h index 5a1e48d9c0..7f9fb026f8 100644 --- a/libraries/shared/src/SharedUtil.h +++ b/libraries/shared/src/SharedUtil.h @@ -216,6 +216,7 @@ QString formatUsecTime(float usecs); QString formatUsecTime(double usecs); QString formatUsecTime(quint64 usecs); QString formatUsecTime(qint64 usecs); +QString formatSecTime(qint64 secs); QString formatSecondsElapsed(float seconds); bool similarStrings(const QString& stringA, const QString& stringB); From fc8e7a0841a875a2b7886784aa407547834326da Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 6 Feb 2018 14:33:01 -0800 Subject: [PATCH 002/157] Add target_zlib to DS CMakeLists.txt --- domain-server/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 0e958b9537..a578be5ff6 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -24,6 +24,8 @@ symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CU # link the shared hifi libraries link_hifi_libraries(embedded-webserver networking shared avatars octree) +target_zlib() + add_dependency_external_projects(quazip) find_package(QuaZip REQUIRED) From ff5be2d690c9cbe3e5bd313daad68976f091d83f Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 7 Feb 2018 09:27:51 -0800 Subject: [PATCH 003/157] Fix entity data ID sometimes being reset --- domain-server/src/DomainServer.cpp | 5 ++++- libraries/octree/src/OctreePersistThread.cpp | 11 +++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index e083710d35..edb3fe77dd 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1802,7 +1802,7 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES"; + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; QFile file(entityFilePath); if (file.open(QIODevice::ReadOnly)) { reply->writePrimitive(true); @@ -3312,6 +3312,9 @@ void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointererror(); if (networkError == QNetworkReply::NoError) { diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 9c9a4d40db..d51bd540bc 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -171,15 +171,13 @@ bool OctreePersistThread::process() { quint64 loadStarted = usecTimestampNow(); qCDebug(octree) << "loading Octrees from file: " << _filename << "..."; - if (_replacementData.isNull()) { - sendLatestEntityDataToDS(); - } else { + if (!_replacementData.isNull()) { replaceData(_replacementData); - _replacementData.clear(); } OctreeUtils::RawOctreeData data; if (OctreeUtils::readOctreeDataInfoFromFile(_filename, &data)) { + qDebug() << "Setting entity version info to: " << data.id << data.version; _tree->setEntityVersionInfo(data.id, data.version); } @@ -244,6 +242,11 @@ bool OctreePersistThread::process() { // want an uninitialized value for this, so we set it to the current time (startup of the server) time(&_lastPersistTime); + if (_replacementData.isNull()) { + sendLatestEntityDataToDS(); + } + _replacementData.clear(); + emit loadCompleted(); } From 1b7b4eee50064fbeaf062fe810225993dc417fed Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Mon, 12 Feb 2018 11:46:45 -0800 Subject: [PATCH 004/157] Fix entity data not being gzipped when adding id+version --- assignment-client/src/octree/OctreeServer.cpp | 2 +- libraries/octree/src/OctreeUtils.cpp | 12 ++++++++++++ libraries/octree/src/OctreeUtils.h | 1 + 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index e78f9f108b..6704786c36 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1193,7 +1193,7 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes QFile file(_persistAbsoluteFilePath); if (file.open(QIODevice::WriteOnly)) { - auto entityData = data.toByteArray(); + auto entityData = data.toGzippedByteArray(); file.write(entityData); file.close(); } else { diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index d8925a10ca..e068e83b23 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -152,3 +152,15 @@ QByteArray OctreeUtils::RawOctreeData::toByteArray() { return doc.toJson(); } + +QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { + auto data = toByteArray(); + QByteArray gzData; + + if (!gzip(data, gzData, -1)) { + qCritical("Unable to gzip data while converting json."); + return QByteArray(); + } + + return gzData; +} \ No newline at end of file diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index e5c7b617cd..18b0d27883 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -31,6 +31,7 @@ public: QJsonArray octreeData; QByteArray toByteArray(); + QByteArray toGzippedByteArray(); }; bool readOctreeFile(QString path, QJsonDocument* doc); From 2a667fcd60271c6a55147968aec8e0bae70d1bfe Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Tue, 13 Feb 2018 11:36:22 -0800 Subject: [PATCH 005/157] Cleanup entity -> ds persist --- assignment-client/CMakeLists.txt | 1 - assignment-client/src/octree/OctreeServer.cpp | 19 +++++--- assignment-client/src/octree/OctreeServer.h | 2 - .../src/DomainContentBackupManager.cpp | 18 +------ domain-server/src/DomainServer.cpp | 48 +++++++------------ interface/src/Application.cpp | 1 - libraries/entities/src/EntityTree.cpp | 2 +- libraries/octree/CMakeLists.txt | 1 - libraries/octree/src/Octree.cpp | 13 ----- libraries/octree/src/Octree.h | 2 + libraries/octree/src/OctreePersistThread.cpp | 1 + libraries/octree/src/OctreeUtils.cpp | 10 ++++ libraries/octree/src/OctreeUtils.h | 6 ++- 13 files changed, 49 insertions(+), 75 deletions(-) diff --git a/assignment-client/CMakeLists.txt b/assignment-client/CMakeLists.txt index 3de4c5fd3f..c73e8e1d34 100644 --- a/assignment-client/CMakeLists.txt +++ b/assignment-client/CMakeLists.txt @@ -6,7 +6,6 @@ setup_hifi_project(Core Gui Network Script Quick WebSockets) if (APPLE) set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks") endif () -set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "/testing/") setup_memory_debugger() diff --git a/assignment-client/src/octree/OctreeServer.cpp b/assignment-client/src/octree/OctreeServer.cpp index 6704786c36..05d070606a 100644 --- a/assignment-client/src/octree/OctreeServer.cpp +++ b/assignment-client/src/octree/OctreeServer.cpp @@ -1044,12 +1044,13 @@ void OctreeServer::readConfiguration() { // If persist filename does not exist, let's see if there is one beside the application binary // If there is, let's copy it over to our target persist directory QDir persistPath { _persistFilePath }; - _persistAbsoluteFilePath = persistPath.absolutePath(); if (persistPath.isRelative()) { // if the domain settings passed us a relative path, make an absolute path that is relative to the // default data directory _persistAbsoluteFilePath = QDir(PathUtils::getAppDataFilePath("entities/")).absoluteFilePath(_persistFilePath); + } else { + _persistAbsoluteFilePath = persistPath.absolutePath(); } qDebug() << "persistFilePath=" << _persistFilePath; @@ -1174,6 +1175,11 @@ void OctreeServer::domainSettingsRequestComplete() { } void OctreeServer::handleOctreeDataFileReply(QSharedPointer message) { + if (_state != OctreeServerState::WaitingForOctreeDataNegotation) { + qCWarning(octree_server) << "Server received ocree data file reply but is not currently negotiating."; + return; + } + bool includesNewData; message->readPrimitive(&includesNewData); QByteArray replaceData; @@ -1188,8 +1194,7 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes if (OctreeUtils::readOctreeDataInfoFromFile(_persistAbsoluteFilePath, &data)) { if (data.id.isNull()) { qCDebug(octree_server) << "Current octree data has a null id, updating"; - data.id = QUuid::createUuid(); - data.version = 0; + data.resetIdAndVersion(); QFile file(_persistAbsoluteFilePath); if (file.open(QIODevice::WriteOnly)) { @@ -1202,17 +1207,17 @@ void OctreeServer::handleOctreeDataFileReply(QSharedPointer mes } } } + + _state = OctreeServerState::Running; beginRunning(replaceData); } void OctreeServer::beginRunning(QByteArray replaceData) { - if (_state == OctreeServerState::Running) { - qCWarning(octree_server) << "Server is already running"; + if (_state != OctreeServerState::Running) { + qCWarning(octree_server) << "Server is not running"; return; } - _state = OctreeServerState::Running; - auto nodeList = DependencyManager::get(); // we need to ask the DS about agents so we can ping/reply with them diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index 6f77920ee0..eab71647e3 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -149,8 +149,6 @@ private slots: void domainSettingsRequestComplete(); void handleOctreeQueryPacket(QSharedPointer message, SharedNodePointer senderNode); void handleOctreeDataNackPacket(QSharedPointer message, SharedNodePointer senderNode); - //void handleOctreeFileReplacement(QSharedPointer message); - //void handleOctreeFileReplacementFromURL(QSharedPointer message); void handleOctreeDataFileReply(QSharedPointer message); void removeSendThread(); diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 0eca10f8af..4f544d7011 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -279,23 +279,9 @@ void DomainContentBackupManager::backup() { qDebug() << "Created backup: " << fileName; - removeOldBackupVersions(rule); + rule.lastBackupSeconds = nowSeconds; - if (rule.maxBackupVersions > 0) { - // Execute backup - auto result = true; - if (result) { - qCDebug(domain_server) << "DONE backing up persist file..."; - rule.lastBackupSeconds = nowSeconds; - } else { - qCDebug(domain_server) << "ERROR in backing up persist file..."; - perror("ERROR in backing up persist file"); - } - } else { - qCDebug(domain_server) << "This backup rule" << rule.name << " has Max Rolled Backup Versions less than 1 [" - << rule.maxBackupVersions << "]." - << " There are no backups to be done..."; - } + removeOldBackupVersions(rule); } else { qCDebug(domain_server) << "Backup not needed for this rule [" << rule.name << "]..."; } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index edb3fe77dd..8f0e26375e 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -289,7 +289,6 @@ DomainServer::DomainServer(int argc, char* argv[]) : } } - qDebug() << "Starting persist thread"; if (QDir(getEntitiesDirPath()).mkpath(".")) { qCDebug(domain_server) << "Created entities data directory"; } @@ -1785,7 +1784,8 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { - auto idData = message->read(16); + constexpr size_t UUID_SIZE_BYTES = 16; + auto idData = message->read(UUID_SIZE_BYTES); id = QUuid::fromRfc4122(idData); message->readPrimitive(&version); qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; @@ -1794,7 +1794,6 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointer(); - limitedNodeList->eachMatchingNode([](const SharedNodePointer& node) { - return node->getType() == NodeType::EntityServer && node->getActiveSocket(); - }, [&octreeFile, limitedNodeList](const SharedNodePointer& octreeNode) { - // setup a packet to send to this octree server with the new octree file data - auto octreeFilePacketList = NLPacketList::create(PacketType::OctreeFileReplacement, QByteArray(), true, true); - octreeFilePacketList->write(octreeFile); - - qDebug() << "Sending an octree file replacement of" << octreeFile.size() << "bytes to" << octreeNode; - - limitedNodeList->sendPacketList(std::move(octreeFilePacketList), *octreeNode); - }); } void DomainServer::handleOctreeFileReplacementFromURLRequest(QSharedPointer message) { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c031c0e8d4..bc44bb4cf0 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -6247,7 +6247,6 @@ bool Application::askToReplaceDomainContent(const QString& url) { }, [&urlData, limitedNodeList, &domainHandler](const SharedNodePointer& octreeNode) { auto octreeFilePacket = NLPacket::create(PacketType::OctreeFileReplacementFromUrl, urlData.size(), true); octreeFilePacket->write(urlData); - qDebug() << "WRiting url data: " << urlData; limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); }); auto addressManager = DependencyManager::get(); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4f96a6d072..f632bcf140 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2244,7 +2244,7 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer if (! entityDescription.contains("Entities")) { entityDescription["Entities"] = QVariantList(); } - entityDescription["DataVersion"] = ++_persistDataVersion; + entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, diff --git a/libraries/octree/CMakeLists.txt b/libraries/octree/CMakeLists.txt index 228779dbba..bea036add3 100644 --- a/libraries/octree/CMakeLists.txt +++ b/libraries/octree/CMakeLists.txt @@ -1,4 +1,3 @@ set(TARGET_NAME octree) -include_directories(system /usr/local/Cellar/qt5/5.9.1/include) setup_hifi_library() link_hifi_libraries(shared networking) diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp index 23352a548c..d62cbad765 100644 --- a/libraries/octree/src/Octree.cpp +++ b/libraries/octree/src/Octree.cpp @@ -1757,19 +1757,6 @@ bool Octree::readJSONFromStream(uint64_t streamLength, QDataStream& inputStream, QVariant asVariant = asDocument.toVariant(); QVariantMap asMap = asVariant.toMap(); bool success = readFromMap(asMap); - /* - if (success) { - if (asMap.contains("DataVersion") && asMap.contains("Id")) { - bool versionOk; - auto dataVersion = asMap["DataVersion"].toLongLong(&versionOk); - if (versionOk) { - auto id = asMap["Id"].toUuid(); - _persistDataVersion = dataVersion; - _persistID = id; - } - } - } - */ delete[] rawData; return success; } diff --git a/libraries/octree/src/Octree.h b/libraries/octree/src/Octree.h index 1b9495717b..8954e53f8b 100644 --- a/libraries/octree/src/Octree.h +++ b/libraries/octree/src/Octree.h @@ -341,6 +341,8 @@ public: virtual quint64 getAverageLoggingTime() const { return 0; } virtual quint64 getAverageFilterTime() const { return 0; } + void incrementPersistDataVersion() { _persistDataVersion++; } + signals: void importSize(float x, float y, float z); void importProgress(int progress); diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index d51bd540bc..a669b7d3bb 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -311,6 +311,7 @@ void OctreePersistThread::persist() { backup(); // handle backup if requested qCDebug(octree) << "persist operation DONE with backup..."; + _tree->incrementPersistDataVersion(); // create our "lock" file to indicate we're saving. QString lockFileName = _filename + ".lock"; diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index e068e83b23..85ea3beb86 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -80,6 +80,9 @@ float getOrthographicAccuracySize(float octreeSizeScale, int boundaryLevelAdjust return (smallestSize * MAX_VISIBILITY_DISTANCE_FOR_UNIT_ELEMENT) / boundaryDistanceForRenderLevel(boundaryLevelAdjust, octreeSizeScale); } +// Reads octree file and parses it into a QJsonDocument. Handles both gzipped and non-gzipped files. +// Returns true if the file was successfully opened and parsed, otherwise false. +// Example failures: file does not exist, gzipped file cannot be unzipped, invalid JSON. bool OctreeUtils::readOctreeFile(QString path, QJsonDocument* doc) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { @@ -129,6 +132,8 @@ bool OctreeUtils::readOctreeDataInfoFromData(QByteArray data, OctreeUtils::RawOc return readOctreeDataInfoFromJSON(root, octreeData); } +// Reads octree file and parses it into a RawOctreeData object. +// Returns false if readOctreeFile fails. bool OctreeUtils::readOctreeDataInfoFromFile(QString path, OctreeUtils::RawOctreeData* octreeData) { QJsonDocument doc; if (!OctreeUtils::readOctreeFile(path, &doc)) { @@ -163,4 +168,9 @@ QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { } return gzData; +} + +void OctreeUtils::RawOctreeData::resetIdAndVersion() { + id = QUuid::createUuid(); + version = OctreeUtils::INITIAL_VERSION; } \ No newline at end of file diff --git a/libraries/octree/src/OctreeUtils.h b/libraries/octree/src/OctreeUtils.h index 18b0d27883..6fb0e62bcb 100644 --- a/libraries/octree/src/OctreeUtils.h +++ b/libraries/octree/src/OctreeUtils.h @@ -22,14 +22,18 @@ class QJsonDocument; namespace OctreeUtils { +using Version = int64_t; +constexpr Version INITIAL_VERSION = 0; + // RawOctreeData is an intermediate format between JSON and a fully deserialized Octree. class RawOctreeData { public: QUuid id { QUuid() }; - int64_t version { -1 }; + Version version { -1 }; QJsonArray octreeData; + void resetIdAndVersion(); QByteArray toByteArray(); QByteArray toGzippedByteArray(); }; From 0bbbff95cd2bd33bd6a0cad70ce351d9e14454c6 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 11:38:18 -0800 Subject: [PATCH 006/157] Fix replacement octree data not working --- assignment-client/src/octree/OctreeServer.h | 2 -- domain-server/src/DomainServer.cpp | 22 ++++++++++++++------- libraries/octree/src/OctreeUtils.cpp | 8 ++------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/octree/OctreeServer.h b/assignment-client/src/octree/OctreeServer.h index eab71647e3..e7efc731f2 100644 --- a/assignment-client/src/octree/OctreeServer.h +++ b/assignment-client/src/octree/OctreeServer.h @@ -176,8 +176,6 @@ protected: UniqueSendThread createSendThread(const SharedNodePointer& node); virtual UniqueSendThread newSendThread(const SharedNodePointer& node); - //void replaceContentFromMessageData(QByteArray content); - int _argc; const char** _argv; char** _parsedArgV; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8f0e26375e..3eb1f21da0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -3234,15 +3234,23 @@ void DomainServer::maybeHandleReplacementEntityFile() { } else { qCDebug(domain_server) << "Replacing existing entity date with replacement file"; - data.resetIdAndVersion(); - auto gzippedData = data.toGzippedByteArray(); - - QFile currentFile(getEntitiesFilePath()); - if (!currentFile.open(QIODevice::WriteOnly)) { + QFile replacementFile(replacementFilePath); + if (!replacementFile.remove()) { + // If we can't remove the replacement file, we are at risk of getting into a state where + // we continually replace the primary entity file with the replacement entity file. qCWarning(domain_server) - << "Failed to update entities data file with replacement file, unable to open entities file for writing"; + << "Unable to remove replacement file, bailing"; } else { - currentFile.write(gzippedData); + data.resetIdAndVersion(); + auto gzippedData = data.toGzippedByteArray(); + + QFile currentFile(getEntitiesFilePath()); + if (!currentFile.open(QIODevice::WriteOnly)) { + qCWarning(domain_server) + << "Failed to update entities data file with replacement file, unable to open entities file for writing"; + } else { + currentFile.write(gzippedData); + } } } } diff --git a/libraries/octree/src/OctreeUtils.cpp b/libraries/octree/src/OctreeUtils.cpp index 85ea3beb86..739c2661b3 100644 --- a/libraries/octree/src/OctreeUtils.cpp +++ b/libraries/octree/src/OctreeUtils.cpp @@ -93,12 +93,7 @@ bool OctreeUtils::readOctreeFile(QString path, QJsonDocument* doc) { QByteArray data = file.readAll(); QByteArray jsonData; - if (path.endsWith(".json.gz")) { - if (!gunzip(data, jsonData)) { - qCritical() << "json File not in gzip format: " << path; - return false; - } - } else { + if (!gunzip(data, jsonData)) { jsonData = data; } @@ -173,4 +168,5 @@ QByteArray OctreeUtils::RawOctreeData::toGzippedByteArray() { void OctreeUtils::RawOctreeData::resetIdAndVersion() { id = QUuid::createUuid(); version = OctreeUtils::INITIAL_VERSION; + qDebug() << "Reset octree data to: " << id << version; } \ No newline at end of file From 11b7fb89a903f044356b275f45cfeda21e883665 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 6 Feb 2018 15:37:48 -0800 Subject: [PATCH 007/157] Integrate new backup systems --- domain-server/src/BackupHandler.h | 110 ++++++++++++++++++ domain-server/src/BackupSupervisor.cpp | 94 +++++++++------ domain-server/src/BackupSupervisor.h | 66 ++++++++++- .../src/DomainContentBackupManager.cpp | 15 ++- .../src/DomainContentBackupManager.h | 17 +-- domain-server/src/DomainServer.cpp | 20 +--- domain-server/src/DomainServer.h | 2 + 7 files changed, 251 insertions(+), 73 deletions(-) create mode 100644 domain-server/src/BackupHandler.h diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h new file mode 100644 index 0000000000..c8e90025f8 --- /dev/null +++ b/domain-server/src/BackupHandler.h @@ -0,0 +1,110 @@ +// +// BackupHandler.h +// assignment-client +// +// Created by Clement Brisset on 2/5/18. +// Copyright 2018 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 +// + +#ifndef hifi_BackupHandler_h +#define hifi_BackupHandler_h + +#include + +#include + +#include + +class BackupHandler { +public: + template + BackupHandler(T x) : _self(std::make_shared>(std::move(x))) {} + + void loadBackup(const QuaZip& zip) { + _self->loadBackup(zip); + } + void createBackup(QuaZip& zip) const { + _self->createBackup(zip); + } + void recoverBackup(const QuaZip& zip) const { + _self->recoverBackup(zip); + } + void deleteBackup(const QuaZip& zip) { + _self->deleteBackup(zip); + } + void consolidateBackup(QuaZip& zip) const { + _self->consolidateBackup(zip); + } + +private: + struct Concept { + virtual ~Concept() = default; + + virtual void loadBackup(const QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) const = 0; + virtual void recoverBackup(const QuaZip& zip) const = 0; + virtual void deleteBackup(const QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) const = 0; + }; + + template + struct Model : Concept { + Model(T x) : data(std::move(x)) {} + + void loadBackup(const QuaZip& zip) { + data.loadBackup(zip); + } + void createBackup(QuaZip& zip) const { + data.createBackup(zip); + } + void recoverBackup(const QuaZip& zip) const { + data.recoverBackup(zip); + } + void deleteBackup(const QuaZip& zip) { + data.deleteBackup(zip); + } + void consolidateBackup(QuaZip& zip) const { + data.consolidateBackup(zip); + } + + T data; + }; + + std::shared_ptr _self; +}; + +#include +class EntitiesBackupHandler { +public: + EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} + + void loadBackup(const QuaZip& zip) {} + + void createBackup(QuaZip& zip) const { + qDebug() << "Creating a backup from handler"; + + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + } + + void recoverBackup(const QuaZip& zip) const {} + void deleteBackup(const QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) const {} + +private: + QString _entitiesFilePath; +}; + +#endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 03ad5de558..95fb1c6a6d 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -40,6 +40,39 @@ BackupSupervisor::BackupSupervisor() { } loadAllBackups(); + + static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; + _mappingsRefreshTimer.setInterval(MAPPINGS_REFRESH_INTERVAL); + _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); + _mappingsRefreshTimer.setSingleShot(false); + QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); + _mappingsRefreshTimer.start(); +} + +void BackupSupervisor::refreshMappings() { + auto assetClient = DependencyManager::get(); + auto request = assetClient->createGetAllMappingsRequest(); + + QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { + if (request->getError() == MappingRequest::NoError) { + const auto& mappings = request->getMappings(); + + qDebug() << "Refreshed" << mappings.size() << "asset mappings!"; + + _currentMappings.clear(); + for (const auto& mapping : mappings) { + _currentMappings.insert({ mapping.first, mapping.second.hash }); + } + _lastMappingsRefresh = usecTimestampNow(); + } else { + qCritical() << "Could not refresh asset server mappings."; + qCritical() << " Error:" << request->getErrorString(); + } + + request->deleteLater(); + }); + + request->start(); } void BackupSupervisor::loadAllBackups() { @@ -138,35 +171,26 @@ void BackupSupervisor::backupAssetServer() { return; } - auto assetClient = DependencyManager::get(); - auto request = assetClient->createGetAllMappingsRequest(); + if (_lastMappingsRefresh == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } - connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { - qDebug() << "Got" << request->getMappings().size() << "mappings!"; - - if (request->getError() != MappingRequest::NoError) { - qCritical() << "Could not complete backup."; - qCritical() << " Error:" << request->getErrorString(); - finishBackup(); - request->deleteLater(); - return; - } - - if (!writeBackupFile(request->getMappings())) { - finishBackup(); - request->deleteLater(); - return; - } - - assert(!_backups.empty()); - const auto& mappings = _backups.back().mappings; - backupMissingFiles(mappings); - - request->deleteLater(); - }); + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } startBackup(); - request->start(); + + if (!writeBackupFile(_currentMappings)) { + finishBackup(); + return; + } + + assert(!_backups.empty()); + const auto& mappings = _backups.back().mappings; + backupMissingFiles(mappings); } void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) { @@ -193,7 +217,7 @@ void BackupSupervisor::backupNextMissingFile() { auto assetClient = DependencyManager::get(); auto assetRequest = assetClient->createRequest(hash); - connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { + QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { qDebug() << "Got" << request->getHash(); @@ -213,7 +237,7 @@ void BackupSupervisor::backupNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings) { +bool BackupSupervisor::writeBackupFile(const AssetUtils::Mappings& mappings) { auto filename = MAPPINGS_PREFIX + QDateTime::currentDateTimeUtc().toString(Qt::ISODate) + ".json"; QFile file { PathUtils::getAppDataPath() + BACKUPS_DIR + filename }; if (!file.open(QFile::WriteOnly)) { @@ -224,9 +248,9 @@ bool BackupSupervisor::writeBackupFile(const AssetUtils::AssetMappings& mappings AssetServerBackup backup; QJsonObject jsonObject; for (auto& mapping : mappings) { - backup.mappings[mapping.first] = mapping.second.hash; - _assetsInBackups.insert(mapping.second.hash); - jsonObject.insert(mapping.first, mapping.second.hash); + backup.mappings[mapping.first] = mapping.second; + _assetsInBackups.insert(mapping.second); + jsonObject.insert(mapping.first, mapping.second); } QJsonDocument document(jsonObject); @@ -262,7 +286,7 @@ void BackupSupervisor::restoreAssetServer(int backupIndex) { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); - connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) { + QObject::connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) { if (request->getError() == MappingRequest::NoError) { const auto& newMappings = _backups.at(backupIndex).mappings; computeServerStateDifference(request->getMappings(), newMappings); @@ -332,7 +356,7 @@ void BackupSupervisor::restoreNextAsset() { auto assetClient = DependencyManager::get(); auto request = assetClient->createUpload(assetFilename); - connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { + QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { qCritical() << "Failed to restore asset:" << request->getFilename(); qCritical() << " Error:" << request->getErrorString(); @@ -350,7 +374,7 @@ void BackupSupervisor::updateMappings() { auto assetClient = DependencyManager::get(); for (const auto& mapping : _mappingsLeftToSet) { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); - connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { + QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { qCritical() << "Failed to set mapping:" << request->getPath(); qCritical() << " Error:" << request->getErrorString(); @@ -369,7 +393,7 @@ void BackupSupervisor::updateMappings() { _mappingsLeftToSet.clear(); auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); - connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { + QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { qCritical() << "Failed to delete mappings"; qCritical() << " Error:" << request->getErrorString(); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 067abdc25c..dd293c7fd5 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -16,11 +16,17 @@ #include #include +#include +#include +#include #include +#include #include +class QuaZip; + struct AssetServerBackup { std::string filePath; AssetUtils::Mappings mappings; @@ -42,7 +48,12 @@ public: bool backupInProgress() const { return _backupInProgress; } bool restoreInProgress() const { return _restoreInProgress; } + AssetUtils::Mappings getCurrentMappings() const { return _currentMappings; } + quint64 getLastRefreshTimestamp() const { return _lastMappingsRefresh; } + private: + void refreshMappings(); + void loadAllBackups(); bool loadBackup(const QString& backupFile); @@ -50,7 +61,7 @@ private: void finishBackup() { _backupInProgress = false; } void backupMissingFiles(const AssetUtils::Mappings& mappings); void backupNextMissingFile(); - bool writeBackupFile(const AssetUtils::AssetMappings& mappings); + bool writeBackupFile(const AssetUtils::Mappings& mappings); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); void startRestore() { _restoreInProgress = true; } @@ -64,6 +75,10 @@ private: QString _backupsDirectory; QString _assetsDirectory; + + quint64 _lastMappingsRefresh { 0 }; + AssetUtils::Mappings _currentMappings; + // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -80,6 +95,55 @@ private: std::vector> _mappingsLeftToSet; AssetUtils::AssetPathList _mappingsLeftToDelete; int _mappingRequestsInFlight { 0 }; + + QTimer _mappingsRefreshTimer; +}; + + +#include +class AssetsBackupHandler { +public: + AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} + + void loadBackup(const QuaZip& zip) {} + + void createBackup(QuaZip& zip) const { + quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); + AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); + + if (lastRefreshTimestamp == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } + + QJsonObject jsonObject; + for (const auto& mapping : mappings) { + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { + qDebug() << "testCreate(): outFile.open()"; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + + void recoverBackup(const QuaZip& zip) const {} + void deleteBackup(const QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) const {} + +private: + BackupSupervisor* _backupSupervisor; }; #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 4f544d7011..39ae63bc16 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -37,8 +37,8 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); -void DomainContentBackupManager::addCreateBackupHandler(CreateBackupHandler handler) { - _backupHandlers.push_back(handler); +void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { + _backupHandlers.push_back(std::move(handler)); } DomainContentBackupManager::DomainContentBackupManager(const QString& backupDirectory, @@ -48,7 +48,6 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire : _backupDirectory(backupDirectory), _persistInterval(persistInterval), _initialLoadComplete(false), - _loadTimeUSecs(0), _lastCheck(0), _debugTimestampNow(debugTimestampNow), _lastTimeDebug(0) { @@ -268,14 +267,14 @@ void DomainContentBackupManager::backup() { auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; - auto zip = new QuaZip(_backupDirectory + "/" + fileName); - zip->open(QuaZip::mdAdd); + QuaZip zip(_backupDirectory + "/" + fileName); + zip.open(QuaZip::mdAdd); - for (auto& handler : _backupHandlers) { - handler(zip); + for (const auto& handler : _backupHandlers) { + handler.createBackup(zip); } - zip->close(); + zip.close(); qDebug() << "Created backup: " << fileName; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 20408fe486..67fc51f8f3 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -15,17 +15,11 @@ #define hifi_DomainContentBackupManager_h #include -#include #include -#include -#include +#include -#include - -using BackupResult = std::vector; -using CreateBackupHandler = std::function; -using RecoverBackupHandler = std::function; +#include "BackupHandler.h" class DomainContentBackupManager : public GenericThread { Q_OBJECT @@ -46,9 +40,8 @@ public: int persistInterval = DEFAULT_PERSIST_INTERVAL, bool debugTimestampNow = false); - void addCreateBackupHandler(CreateBackupHandler handler); + void addBackupHandler(BackupHandler handler); bool isInitialLoadComplete() const { return _initialLoadComplete; } - int64_t getLoadElapsedTime() const { return _loadTimeUSecs; } void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -70,12 +63,10 @@ protected: private: QString _backupDirectory; - std::vector _backupHandlers; + std::vector _backupHandlers; int _persistInterval; bool _initialLoadComplete; - int64_t _loadTimeUSecs; - time_t _lastPersistTime; int64_t _lastCheck; bool _wantBackup{ true }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 3eb1f21da0..ed14bf3bdc 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -45,6 +45,7 @@ #include #include +#include "BackupSupervisor.h" #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -293,23 +294,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : qCDebug(domain_server) << "Created entities data directory"; } maybeHandleReplacementEntityFile(); - auto entitiesFilePath = getEntitiesFilePath(); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addCreateBackupHandler([entitiesFilePath](QuaZip* zip) { - qDebug() << "Creating a backup from handler"; - - QFile entitiesFile { entitiesFilePath }; - - if (entitiesFile.open(QIODevice::ReadOnly)) { - QuaZipFile zipFile { zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", entitiesFilePath)); - zipFile.write(entitiesFile.readAll()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to write entities file to backup:" << zipFile.getZipError(); - } - } - }); + _contentManager->addBackupHandler(EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(AssetsBackupHandler(&_backupSupervisor)); _contentManager->initialize(true); } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index ee0350665e..645327225b 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -275,6 +275,8 @@ private: QHash> _pendingOAuthConnections; QThread _assetClientThread; + + BackupSupervisor _backupSupervisor; }; From a6447da64c6fcc0b950564c2fac16cfbbefb611a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Mon, 12 Feb 2018 16:11:42 -0800 Subject: [PATCH 008/157] More Asset Backup work --- domain-server/src/BackupHandler.h | 55 +- domain-server/src/BackupSupervisor.cpp | 489 ++++++++++-------- domain-server/src/BackupSupervisor.h | 96 +--- .../src/DomainContentBackupManager.cpp | 60 ++- .../src/DomainContentBackupManager.h | 20 +- domain-server/src/DomainServer.cpp | 4 +- domain-server/src/DomainServer.h | 2 - 7 files changed, 359 insertions(+), 367 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index c8e90025f8..5c859165b7 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -21,21 +21,21 @@ class BackupHandler { public: template - BackupHandler(T x) : _self(std::make_shared>(std::move(x))) {} + BackupHandler(T* x) : _self(new Model(x)) {} - void loadBackup(const QuaZip& zip) { + void loadBackup(QuaZip& zip) { _self->loadBackup(zip); } - void createBackup(QuaZip& zip) const { + void createBackup(QuaZip& zip) { _self->createBackup(zip); } - void recoverBackup(const QuaZip& zip) const { + void recoverBackup(QuaZip& zip) { _self->recoverBackup(zip); } - void deleteBackup(const QuaZip& zip) { + void deleteBackup(QuaZip& zip) { _self->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) const { + void consolidateBackup(QuaZip& zip) { _self->consolidateBackup(zip); } @@ -43,37 +43,37 @@ private: struct Concept { virtual ~Concept() = default; - virtual void loadBackup(const QuaZip& zip) = 0; - virtual void createBackup(QuaZip& zip) const = 0; - virtual void recoverBackup(const QuaZip& zip) const = 0; - virtual void deleteBackup(const QuaZip& zip) = 0; - virtual void consolidateBackup(QuaZip& zip) const = 0; + virtual void loadBackup(QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) = 0; + virtual void recoverBackup(QuaZip& zip) = 0; + virtual void deleteBackup(QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) = 0; }; template struct Model : Concept { - Model(T x) : data(std::move(x)) {} + Model(T* x) : data(x) {} - void loadBackup(const QuaZip& zip) { - data.loadBackup(zip); + void loadBackup(QuaZip& zip) { + data->loadBackup(zip); } - void createBackup(QuaZip& zip) const { - data.createBackup(zip); + void createBackup(QuaZip& zip) { + data->createBackup(zip); } - void recoverBackup(const QuaZip& zip) const { - data.recoverBackup(zip); + void recoverBackup(QuaZip& zip) { + data->recoverBackup(zip); } - void deleteBackup(const QuaZip& zip) { - data.deleteBackup(zip); + void deleteBackup(QuaZip& zip) { + data->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) const { - data.consolidateBackup(zip); + void consolidateBackup(QuaZip& zip) { + data->consolidateBackup(zip); } - T data; + std::unique_ptr data; }; - std::shared_ptr _self; + std::unique_ptr _self; }; #include @@ -81,12 +81,13 @@ class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} - void loadBackup(const QuaZip& zip) {} + void loadBackup(QuaZip& zip) {} void createBackup(QuaZip& zip) const { qDebug() << "Creating a backup from handler"; QFile entitiesFile { _entitiesFilePath }; + qDebug() << entitiesFile.size(); if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; @@ -99,8 +100,8 @@ public: } } - void recoverBackup(const QuaZip& zip) const {} - void deleteBackup(const QuaZip& zip) {} + void recoverBackup(QuaZip& zip) const {} + void deleteBackup(QuaZip& zip) {} void consolidateBackup(QuaZip& zip) const {} private: diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 95fb1c6a6d..e0f0378fd4 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -13,6 +13,9 @@ #include #include +#include + +#include #include #include @@ -20,33 +23,231 @@ #include #include -const QString BACKUPS_DIR = "backups/"; -const QString ASSETS_DIR = "files/"; -const QString MAPPINGS_PREFIX = "mappings-"; +const QString ASSETS_DIR = "/assets/"; +const QString MAPPINGS_FILE = "mappings.json"; using namespace std; -BackupSupervisor::BackupSupervisor() { - _backupsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR; - QDir backupDir { _backupsDirectory }; - if (!backupDir.exists()) { - backupDir.mkpath("."); - } +Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) +Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); - _assetsDirectory = PathUtils::getAppDataPath() + BACKUPS_DIR + ASSETS_DIR; +BackupSupervisor::BackupSupervisor(const QString& backupDirectory) { + _assetsDirectory = backupDirectory + ASSETS_DIR; QDir assetsDir { _assetsDirectory }; if (!assetsDir.exists()) { assetsDir.mkpath("."); } - loadAllBackups(); + refreshAssetsOnDisk(); - static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; - _mappingsRefreshTimer.setInterval(MAPPINGS_REFRESH_INTERVAL); _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); - _mappingsRefreshTimer.setSingleShot(false); + _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); - _mappingsRefreshTimer.start(); + + auto nodeList = DependencyManager::get(); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // Give the Asset Server some time to bootup. + static constexpr int ASSET_SERVER_BOOTUP_MARGIN = 1 * 1000; + _mappingsRefreshTimer.start(ASSET_SERVER_BOOTUP_MARGIN); + } + }); +} + + +void BackupSupervisor::refreshAssetsOnDisk() { + QDir assetsDir { _assetsDirectory }; + auto assetNames = assetsDir.entryList(QDir::Files); + + // store all valid hashes + copy_if(begin(assetNames), end(assetNames), + inserter(_assetsOnDisk, begin(_assetsOnDisk)), + AssetUtils::isValidHash); + +} + +void BackupSupervisor::refreshAssetsInBackups() { + _assetsInBackups.clear(); + for (const auto& backup : _backups) { + for (const auto& mapping : backup.mappings) { + _assetsInBackups.insert(mapping.second); + } + } +} + +void BackupSupervisor::checkForMissingAssets() { + vector missingAssets; + set_difference(begin(_assetsInBackups), end(_assetsInBackups), + begin(_assetsOnDisk), end(_assetsOnDisk), + back_inserter(missingAssets)); + if (missingAssets.size() > 0) { + qCWarning(backup_supervisor) << "Found" << missingAssets.size() << "assets missing."; + } +} + +void BackupSupervisor::checkForAssetsToDelete() { + vector deprecatedAssets; + set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), + begin(_assetsInBackups), end(_assetsInBackups), + back_inserter(deprecatedAssets)); + + if (deprecatedAssets.size() > 0) { + qCDebug(backup_supervisor) << "Found" << deprecatedAssets.size() << "assets to delete."; + if (_allBackupsLoadedSuccessfully) { + for (const auto& hash : deprecatedAssets) { + QFile::remove(_assetsDirectory + hash); + } + } else { + qCWarning(backup_supervisor) << "Some backups did not load properly, aborting deleting for safety."; + } + } +} + +void BackupSupervisor::loadBackup(QuaZip& zip) { + _backups.push_back({ zip.getZipName().toStdString(), {}, false }); + auto& backup = _backups.back(); + + if (!zip.setCurrentFile(MAPPINGS_FILE)) { + qCCritical(backup_supervisor) << "Failed to find" << MAPPINGS_FILE << "while recovering backup"; + qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QFile::ReadOnly)) { + qCCritical(backup_supervisor) << "Could not open backup file:" << zip.getZipName(); + qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + QJsonParseError error; + auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); + if (document.isNull() || !document.isObject()) { + qCCritical(backup_supervisor) << "Could not parse backup file to JSON object:" << zip.getZipName(); + qCCritical(backup_supervisor) << " Error:" << error.errorString(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + auto jsonObject = document.object(); + for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { + const auto& assetPath = it.key(); + const auto& assetHash = it.value().toString(); + + if (!AssetUtils::isValidHash(assetHash)) { + qCCritical(backup_supervisor) << "Corrupted mapping in backup file" << zip.getZipName() << ":" << it.key(); + backup.corruptedBackup = true; + _allBackupsLoadedSuccessfully = false; + return; + } + + backup.mappings[assetPath] = assetHash; + _assetsInBackups.insert(assetHash); + } + + return; +} + +void BackupSupervisor::createBackup(QuaZip& zip) { + qDebug() << Q_FUNC_INFO; + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is already an operation in progress."; + return; + } + + if (_lastMappingsRefresh == 0) { + qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + } + + AssetServerBackup backup; + backup.filePath = zip.getZipName().toStdString(); + + QJsonObject jsonObject; + for (const auto& mapping : _currentMappings) { + backup.mappings[mapping.first] = mapping.second; + _assetsInBackups.insert(mapping.second); + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { + qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + return; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + return; + } + _backups.push_back(backup); +} + +void BackupSupervisor::recoverBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; + return; + } + + if (_lastMappingsRefresh == 0) { + qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { + qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + } + + startOperation(); + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == zip.getZipName().toStdString(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup"; + stopOperation(); + return; + } + + const auto& newMappings = it->mappings; + computeServerStateDifference(_currentMappings, newMappings); + + restoreAllAssets(); +} + +void BackupSupervisor::deleteBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + return; + } + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == zip.getZipName().toStdString(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup"; + return; + } + + refreshAssetsInBackups(); + checkForAssetsToDelete(); +} + +void BackupSupervisor::consolidateBackup(QuaZip& zip) { + } void BackupSupervisor::refreshMappings() { @@ -57,179 +258,69 @@ void BackupSupervisor::refreshMappings() { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); - qDebug() << "Refreshed" << mappings.size() << "asset mappings!"; + qCDebug(backup_supervisor) << "Refreshed" << mappings.size() << "asset mappings!"; _currentMappings.clear(); for (const auto& mapping : mappings) { _currentMappings.insert({ mapping.first, mapping.second.hash }); } _lastMappingsRefresh = usecTimestampNow(); + + downloadMissingFiles(_currentMappings); } else { - qCritical() << "Could not refresh asset server mappings."; - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Could not refresh asset server mappings."; + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } request->deleteLater(); + + // Launch next mappings request + static constexpr int MAPPINGS_REFRESH_INTERVAL = 30 * 1000; + _mappingsRefreshTimer.start(MAPPINGS_REFRESH_INTERVAL); }); request->start(); } -void BackupSupervisor::loadAllBackups() { - _backups.clear(); - _assetsInBackups.clear(); - _assetsOnDisk.clear(); - _allBackupsLoadedSuccessfully = true; +void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings) { + auto wasEmpty = _assetsLeftToRequest.empty(); - QDir assetsDir { _assetsDirectory }; - auto assetNames = assetsDir.entryList(QDir::Files); - qDebug() << "Loading" << assetNames.size() << "assets."; - - // store all valid hashes - copy_if(begin(assetNames), end(assetNames), - inserter(_assetsOnDisk, begin(_assetsOnDisk)), AssetUtils::isValidHash); - - QDir backupsDir { _backupsDirectory }; - auto files = backupsDir.entryList({ MAPPINGS_PREFIX + "*.json" }, QDir::Files); - qDebug() << "Loading" << files.size() << "backups."; - - for (const auto& fileName : files) { - auto filePath = backupsDir.filePath(fileName); - auto success = loadBackup(filePath); - if (!success) { - qCritical() << "Failed to load backup file" << filePath; - _allBackupsLoadedSuccessfully = false; - } - } - - vector missingAssets; - set_difference(begin(_assetsInBackups), end(_assetsInBackups), - begin(_assetsOnDisk), end(_assetsOnDisk), - back_inserter(missingAssets)); - if (missingAssets.size() > 0) { - qWarning() << "Found" << missingAssets.size() << "assets missing."; - } - - vector deprecatedAssets; - set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), - begin(_assetsInBackups), end(_assetsInBackups), - back_inserter(deprecatedAssets)); - - if (deprecatedAssets.size() > 0) { - qDebug() << "Found" << deprecatedAssets.size() << "assets to delete."; - if (_allBackupsLoadedSuccessfully) { - for (const auto& hash : deprecatedAssets) { - QFile::remove(_assetsDirectory + hash); - } - } else { - qWarning() << "Some backups did not load properly, aborting deleting for safety."; - } - } -} - -bool BackupSupervisor::loadBackup(const QString& backupFile) { - _backups.push_back({ backupFile.toStdString(), {}, false }); - auto& backup = _backups.back(); - - QFile file { backupFile }; - if (!file.open(QFile::ReadOnly)) { - qCritical() << "Could not open backup file:" << backupFile; - backup.corruptedBackup = true; - return false; - } - QJsonParseError error; - auto document = QJsonDocument::fromJson(file.readAll(), &error); - if (document.isNull() || !document.isObject()) { - qCritical() << "Could not parse backup file to JSON object:" << backupFile; - qCritical() << " Error:" << error.errorString(); - backup.corruptedBackup = true; - return false; - } - - auto jsonObject = document.object(); - for (auto it = begin(jsonObject); it != end(jsonObject); ++it) { - const auto& assetPath = it.key(); - const auto& assetHash = it.value().toString(); - - if (!AssetUtils::isValidHash(assetHash)) { - qCritical() << "Corrupted mapping in backup file" << backupFile << ":" << it.key(); - backup.corruptedBackup = true; - return false; - } - - backup.mappings[assetPath] = assetHash; - _assetsInBackups.insert(assetHash); - } - - _backups.push_back(backup); - return true; -} - -void BackupSupervisor::backupAssetServer() { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is already a backup/restore in progress."; - return; - } - - if (_lastMappingsRefresh == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - startBackup(); - - if (!writeBackupFile(_currentMappings)) { - finishBackup(); - return; - } - - assert(!_backups.empty()); - const auto& mappings = _backups.back().mappings; - backupMissingFiles(mappings); -} - -void BackupSupervisor::backupMissingFiles(const AssetUtils::Mappings& mappings) { - _assetsLeftToRequest.reserve(mappings.size()); - for (auto& mapping : mappings) { + for (const auto& mapping : mappings) { const auto& hash = mapping.second; if (_assetsOnDisk.find(hash) == end(_assetsOnDisk)) { - _assetsLeftToRequest.push_back(hash); + _assetsLeftToRequest.insert(hash); } } - backupNextMissingFile(); + // If we were empty, that means no download chain was already going, start one. + if (wasEmpty) { + downloadNextMissingFile(); + } } -void BackupSupervisor::backupNextMissingFile() { +void BackupSupervisor::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { - finishBackup(); return; } - - auto hash = _assetsLeftToRequest.back(); - _assetsLeftToRequest.pop_back(); + auto hash = *begin(_assetsLeftToRequest); auto assetClient = DependencyManager::get(); auto assetRequest = assetClient->createRequest(hash); QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { - qDebug() << "Got" << request->getHash(); + qCDebug(backup_supervisor) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { - qCritical() << "Failed to write asset file" << request->getHash(); + qCCritical(backup_supervisor) << "Failed to write asset file" << request->getHash(); } } else { - qCritical() << "Failed to backup asset" << request->getHash(); + qCCritical(backup_supervisor) << "Failed to backup asset" << request->getHash(); } - backupNextMissingFile(); + _assetsLeftToRequest.erase(request->getHash()); + downloadNextMissingFile(); request->deleteLater(); }); @@ -237,73 +328,27 @@ void BackupSupervisor::backupNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeBackupFile(const AssetUtils::Mappings& mappings) { - auto filename = MAPPINGS_PREFIX + QDateTime::currentDateTimeUtc().toString(Qt::ISODate) + ".json"; - QFile file { PathUtils::getAppDataPath() + BACKUPS_DIR + filename }; - if (!file.open(QFile::WriteOnly)) { - qCritical() << "Could not open backup file" << file.fileName(); - return false; - } - - AssetServerBackup backup; - QJsonObject jsonObject; - for (auto& mapping : mappings) { - backup.mappings[mapping.first] = mapping.second; - _assetsInBackups.insert(mapping.second); - jsonObject.insert(mapping.first, mapping.second); - } - - QJsonDocument document(jsonObject); - file.write(document.toJson()); - - backup.filePath = file.fileName().toStdString(); - _backups.push_back(backup); - - return true; -} - bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::WriteOnly)) { - qCritical() << "Could not open backup file" << file.fileName(); + qCCritical(backup_supervisor) << "Could not open backup file" << file.fileName(); return false; } - file.write(data); + auto bytesWritten = file.write(data); + if (bytesWritten != data.size()) { + qCCritical(backup_supervisor) << "Could not write data to file" << file.fileName(); + file.remove(); + return false; + } _assetsOnDisk.insert(hash); return true; } -void BackupSupervisor::restoreAssetServer(int backupIndex) { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is already a backup/restore in progress."; - return; - } - - auto assetClient = DependencyManager::get(); - auto request = assetClient->createGetAllMappingsRequest(); - - QObject::connect(request, &GetAllMappingsRequest::finished, this, [this, backupIndex](GetAllMappingsRequest* request) { - if (request->getError() == MappingRequest::NoError) { - const auto& newMappings = _backups.at(backupIndex).mappings; - computeServerStateDifference(request->getMappings(), newMappings); - - restoreAllAssets(); - } else { - finishRestore(); - } - - request->deleteLater(); - }); - - startRestore(); - request->start(); -} - -void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings, +void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings) { _mappingsLeftToSet.reserve((int)newMappings.size()); _assetsLeftToUpload.reserve((int)newMappings.size()); @@ -312,7 +357,7 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi set currentAssets; for (const auto& currentMapping : currentMappings) { const auto& currentPath = currentMapping.first; - const auto& currentHash = currentMapping.second.hash; + const auto& currentHash = currentMapping.second; if (newMappings.find(currentPath) == end(newMappings)) { _mappingsLeftToDelete.push_back(currentPath); @@ -325,7 +370,7 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi const auto& newHash = newMapping.second; auto it = currentMappings.find(newPath); - if (it == end(currentMappings) || it->second.hash != newHash) { + if (it == end(currentMappings) || it->second != newHash) { _mappingsLeftToSet.push_back({ newPath, newHash }); } if (currentAssets.find(newHash) == end(currentAssets)) { @@ -333,9 +378,9 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::AssetMappi } } - qDebug() << "Mappings to set:" << _mappingsLeftToSet.size(); - qDebug() << "Mappings to del:" << _mappingsLeftToDelete.size(); - qDebug() << "Assets to upload:" << _assetsLeftToUpload.size(); + qCDebug(backup_supervisor) << "Mappings to set:" << _mappingsLeftToSet.size(); + qCDebug(backup_supervisor) << "Mappings to del:" << _mappingsLeftToDelete.size(); + qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); } void BackupSupervisor::restoreAllAssets() { @@ -358,8 +403,8 @@ void BackupSupervisor::restoreNextAsset() { QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { - qCritical() << "Failed to restore asset:" << request->getFilename(); - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to restore asset:" << request->getFilename(); + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } restoreNextAsset(); @@ -376,12 +421,12 @@ void BackupSupervisor::updateMappings() { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to set mapping:" << request->getPath(); - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to set mapping:" << request->getPath(); + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { - finishRestore(); + stopOperation(); } request->deleteLater(); @@ -395,12 +440,12 @@ void BackupSupervisor::updateMappings() { auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCritical() << "Failed to delete mappings"; - qCritical() << " Error:" << request->getErrorString(); + qCCritical(backup_supervisor) << "Failed to delete mappings"; + qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { - finishRestore(); + stopOperation(); } request->deleteLater(); @@ -410,15 +455,3 @@ void BackupSupervisor::updateMappings() { request->start(); ++_mappingRequestsInFlight; } -bool BackupSupervisor::deleteBackup(int backupIndex) { - if (backupInProgress() || restoreInProgress()) { - qWarning() << "There is a backup/restore in progress."; - return false; - } - const auto& filePath = _backups.at(backupIndex).filePath; - auto success = QFile::remove(filePath.c_str()); - - loadAllBackups(); - - return success; -} diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index dd293c7fd5..a89be66742 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -37,48 +37,45 @@ class BackupSupervisor : public QObject { Q_OBJECT public: - BackupSupervisor(); + BackupSupervisor(const QString& backupDirectory); - void backupAssetServer(); - void restoreAssetServer(int backupIndex); - bool deleteBackup(int backupIndex); + void loadBackup(QuaZip& zip); + void createBackup(QuaZip& zip); + void recoverBackup(QuaZip& zip); + void deleteBackup(QuaZip& zip); + void consolidateBackup(QuaZip& zip); - const std::vector& getBackups() const { return _backups; }; - - bool backupInProgress() const { return _backupInProgress; } - bool restoreInProgress() const { return _restoreInProgress; } - - AssetUtils::Mappings getCurrentMappings() const { return _currentMappings; } - quint64 getLastRefreshTimestamp() const { return _lastMappingsRefresh; } + bool operationInProgress() const { return _operationInProgress; } private: void refreshMappings(); - void loadAllBackups(); - bool loadBackup(const QString& backupFile); + void refreshAssetsInBackups(); + void refreshAssetsOnDisk(); + void checkForMissingAssets(); + void checkForAssetsToDelete(); - void startBackup() { _backupInProgress = true; } - void finishBackup() { _backupInProgress = false; } - void backupMissingFiles(const AssetUtils::Mappings& mappings); - void backupNextMissingFile(); - bool writeBackupFile(const AssetUtils::Mappings& mappings); + void startOperation() { _operationInProgress = true; } + void stopOperation() { _operationInProgress = false; } + + void downloadMissingFiles(const AssetUtils::Mappings& mappings); + void downloadNextMissingFile(); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); - void startRestore() { _restoreInProgress = true; } - void finishRestore() { _restoreInProgress = false; } - void computeServerStateDifference(const AssetUtils::AssetMappings& currentMappings, + void computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings); void restoreAllAssets(); void restoreNextAsset(); void updateMappings(); - QString _backupsDirectory; QString _assetsDirectory; - + QTimer _mappingsRefreshTimer; quint64 _lastMappingsRefresh { 0 }; AssetUtils::Mappings _currentMappings; + bool _operationInProgress { false }; + // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -86,64 +83,13 @@ private: std::set _assetsOnDisk; // Internal storage for backup in progress - bool _backupInProgress { false }; - std::vector _assetsLeftToRequest; + std::set _assetsLeftToRequest; // Internal storage for restore in progress - bool _restoreInProgress { false }; std::vector _assetsLeftToUpload; std::vector> _mappingsLeftToSet; AssetUtils::AssetPathList _mappingsLeftToDelete; int _mappingRequestsInFlight { 0 }; - - QTimer _mappingsRefreshTimer; -}; - - -#include -class AssetsBackupHandler { -public: - AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} - - void loadBackup(const QuaZip& zip) {} - - void createBackup(QuaZip& zip) const { - quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); - AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); - - if (lastRefreshTimestamp == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - QJsonObject jsonObject; - for (const auto& mapping : mappings) { - jsonObject.insert(mapping.first, mapping.second); - } - QJsonDocument document(jsonObject); - - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { - qDebug() << "testCreate(): outFile.open()"; - } - zipFile.write(document.toJson()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); - } - } - - void recoverBackup(const QuaZip& zip) const {} - void deleteBackup(const QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) const {} - -private: - BackupSupervisor* _backupSupervisor; }; #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 39ae63bc16..7c1c7f2cd7 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -47,10 +47,7 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire bool debugTimestampNow) : _backupDirectory(backupDirectory), _persistInterval(persistInterval), - _initialLoadComplete(false), - _lastCheck(0), - _debugTimestampNow(debugTimestampNow), - _lastTimeDebug(0) { + _lastCheck(usecTimestampNow()) { parseSettings(settings); } @@ -101,7 +98,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { qCDebug(domain_server) << " lastBackup: NEVER"; } - _backupRules << newRule; + _backupRules.push_back(newRule); } } else { qCDebug(domain_server) << "BACKUP RULES: NONE"; @@ -123,6 +120,10 @@ int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& return mostRecentBackupInSecs; } +void DomainContentBackupManager::setup() { + load(); +} + bool DomainContentBackupManager::process() { if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; @@ -139,18 +140,6 @@ bool DomainContentBackupManager::process() { } } - // if we were asked to debugTimestampNow do that now... - if (_debugTimestampNow) { - - quint64 now = usecTimestampNow(); - quint64 sinceLastDebug = now - _lastTimeDebug; - quint64 DEBUG_TIMESTAMP_INTERVAL = 600000000; // every 10 minutes - - if (sinceLastDebug > DEBUG_TIMESTAMP_INTERVAL) { - _lastTimeDebug = usecTimestampNow(true); // ask for debug output - } - } - return isStillRunning(); } @@ -250,6 +239,36 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } } +void DomainContentBackupManager::load() { + QDir backupDir { _backupDirectory }; + if (backupDir.exists()) { + + auto matchingFiles = backupDir.entryInfoList({ "backup-*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + + for (const auto& file : matchingFiles) { + QFile backupFile { file.absoluteFilePath() }; + if (!backupFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open file:" << file.absoluteFilePath(); + qCritical() << " ERROR:" << backupFile.errorString(); + continue; + } + + QuaZip zip { &backupFile }; + if (!zip.open(QuaZip::mdUnzip)) { + qCritical() << "Could not open backup archive:" << file.absoluteFilePath(); + qCritical() << " ERROR:" << zip.getZipError(); + continue; + } + + for (auto& handler : _backupHandlers) { + handler.loadBackup(zip); + } + + zip.close(); + } + } +} + void DomainContentBackupManager::backup() { auto nowDateTime = QDateTime::currentDateTime(); auto nowSeconds = nowDateTime.toSecsSinceEpoch(); @@ -268,9 +287,12 @@ void DomainContentBackupManager::backup() { auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); - zip.open(QuaZip::mdAdd); + if (!zip.open(QuaZip::mdAdd)) { + qDebug() << "Could not open archive"; + } - for (const auto& handler : _backupHandlers) { + for (auto& handler : _backupHandlers) { + qDebug() << "Backup handler"; handler.createBackup(zip); } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 67fc51f8f3..bb1d4f0116 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -41,20 +41,18 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); - bool isInitialLoadComplete() const { return _initialLoadComplete; } void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); -signals: - void loadCompleted(); - protected: /// Implements generic processing behavior for this thread. - bool process() override; + virtual void setup() override; + virtual bool process() override; void persist(); + void load(); void backup(); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); @@ -64,16 +62,10 @@ protected: private: QString _backupDirectory; std::vector _backupHandlers; - int _persistInterval; - bool _initialLoadComplete; + int _persistInterval { 0 }; - time_t _lastPersistTime; - int64_t _lastCheck; - bool _wantBackup{ true }; - QVector _backupRules; - - bool _debugTimestampNow; - int64_t _lastTimeDebug; + int64_t _lastCheck { 0 }; + std::vector _backupRules; }; #endif // hifi_DomainContentBackupManager_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index ed14bf3bdc..06d3549ff8 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(EntitiesBackupHandler(getEntitiesFilePath())); - _contentManager->addBackupHandler(AssetsBackupHandler(&_backupSupervisor)); + _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); } diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 645327225b..ee0350665e 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -275,8 +275,6 @@ private: QHash> _pendingOAuthConnections; QThread _assetClientThread; - - BackupSupervisor _backupSupervisor; }; From 272f95efa294f24ddf91cc1b36af5cf278557925 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 18:13:07 -0800 Subject: [PATCH 009/157] Specify wich packet can ignore verification at DS level --- libraries/networking/src/LimitedNodeList.cpp | 6 ++++-- libraries/networking/src/udt/PacketHeaders.h | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 3516fe948a..9dbbc570dd 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -315,8 +315,10 @@ bool LimitedNodeList::packetSourceAndHashMatchAndTrackBandwidth(const udt::Packe } if (sourceNode) { - if (!PacketTypeEnum::getNonVerifiedPackets().contains(headerType) && - !isDomainServer()) { + bool verifiedPacket = !PacketTypeEnum::getNonVerifiedPackets().contains(headerType); + bool ignoreVerification = isDomainServer() && PacketTypeEnum::getDomainIgnoredVerificationPackets().contains(headerType); + + if (verifiedPacket && !ignoreVerification) { QByteArray packetHeaderHash = NLPacket::verificationHashInHeader(packet); QByteArray expectedHash = NLPacket::hashForPacketAndSecret(packet, sourceNode->getConnectionSecret()); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 7cd02608a1..b263823fa4 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -187,14 +187,19 @@ public: const static QSet getDomainSourcedPackets() { const static QSet DOMAIN_SOURCED_PACKETS = QSet() - << PacketTypeEnum::Value::AssetMappingOperation - << PacketTypeEnum::Value::AssetMappingOperationReply - << PacketTypeEnum::Value::AssetGet - << PacketTypeEnum::Value::AssetGetReply - << PacketTypeEnum::Value::AssetUpload - << PacketTypeEnum::Value::AssetUploadReply; + << PacketTypeEnum::Value::AssetMappingOperation + << PacketTypeEnum::Value::AssetGet + << PacketTypeEnum::Value::AssetUpload; return DOMAIN_SOURCED_PACKETS; } + + const static QSet getDomainIgnoredVerificationPackets() { + const static QSet DOMAIN_IGNORED_VERIFICATION_PACKETS = QSet() + << PacketTypeEnum::Value::AssetMappingOperationReply + << PacketTypeEnum::Value::AssetGetReply + << PacketTypeEnum::Value::AssetUploadReply; + return DOMAIN_IGNORED_VERIFICATION_PACKETS; + } }; using PacketType = PacketTypeEnum::Value; From c41ad1a699c79f6e9a1ec2bd7b6dbe7a2a0ba33f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 15:59:51 -0800 Subject: [PATCH 010/157] Add consolidate --- domain-server/src/BackupSupervisor.cpp | 46 +++++++++++++++++-- domain-server/src/BackupSupervisor.h | 2 +- .../src/DomainContentBackupManager.cpp | 28 +++++++++++ .../src/DomainContentBackupManager.h | 1 + 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index e0f0378fd4..4cb42787ba 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -104,7 +104,7 @@ void BackupSupervisor::checkForAssetsToDelete() { } void BackupSupervisor::loadBackup(QuaZip& zip) { - _backups.push_back({ zip.getZipName().toStdString(), {}, false }); + _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { @@ -171,7 +171,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { } AssetServerBackup backup; - backup.filePath = zip.getZipName().toStdString(); + backup.filePath = zip.getZipName(); QJsonObject jsonObject; for (const auto& mapping : _currentMappings) { @@ -214,7 +214,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { startOperation(); auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName().toStdString(); + return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { qCDebug(backup_supervisor) << "Could not find backup"; @@ -235,7 +235,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { } auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName().toStdString(); + return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { qCDebug(backup_supervisor) << "Could not find backup"; @@ -247,6 +247,44 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { } void BackupSupervisor::consolidateBackup(QuaZip& zip) { + if (operationInProgress()) { + qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + return; + } + QFileInfo zipInfo(zip.getZipName()); + + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + QFileInfo info(value.filePath); + return info.fileName() == zipInfo.fileName(); + }); + if (it == end(_backups)) { + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName(); + return; + } + + for (const auto& mapping : it->mappings) { + const auto& hash = mapping.second; + + QDir assetsDir { _assetsDirectory }; + QFile file { assetsDir.filePath(hash) }; + if (!file.open(QFile::ReadOnly)) { + qCCritical(backup_supervisor) << "Could not open asset file" << file.fileName(); + continue; + } + + QuaZipFile zipFile { &zip }; + static const QString ZIP_ASSETS_FOLDER = "files/"; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { + qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + continue; + } + zipFile.write(file.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + continue; + } + } } diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index a89be66742..d0f6e52ac6 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -28,7 +28,7 @@ class QuaZip; struct AssetServerBackup { - std::string filePath; + QString filePath; AssetUtils::Mappings mappings; bool corruptedBackup; }; diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 7c1c7f2cd7..3f9b3f20d8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -308,3 +308,31 @@ void DomainContentBackupManager::backup() { } } } + +void DomainContentBackupManager::consolidate(QString fileName) { + QDir backupDir { _backupDirectory }; + if (backupDir.exists()) { + auto filePath = backupDir.absoluteFilePath(fileName); + + auto copyFilePath = QDir::tempPath() + "/" + fileName; + + auto copySuccess = QFile::copy(filePath, copyFilePath); + if (!copySuccess) { + qCritical() << "Failed to create full backup."; + return; + } + + QuaZip zip(copyFilePath); + if (!zip.open(QuaZip::mdAdd)) { + qCritical() << "Could not open backup archive:" << filePath; + qCritical() << " ERROR:" << zip.getZipError(); + return; + } + + for (auto& handler : _backupHandlers) { + handler.consolidateBackup(zip); + } + + zip.close(); + } +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index bb1d4f0116..69163b4ead 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -54,6 +54,7 @@ protected: void persist(); void load(); void backup(); + void consolidate(QString fileName); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); int64_t getMostRecentBackupTimeInSecs(const QString& format); From d4b4c55673744e144dcb54938ff75cdb0e9e169f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Tue, 13 Feb 2018 18:38:13 -0800 Subject: [PATCH 011/157] Remove unecessary debug --- domain-server/src/BackupHandler.h | 3 --- domain-server/src/BackupSupervisor.cpp | 1 - domain-server/src/DomainContentBackupManager.cpp | 1 - 3 files changed, 5 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 5c859165b7..332afe22f7 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -84,10 +84,7 @@ public: void loadBackup(QuaZip& zip) {} void createBackup(QuaZip& zip) const { - qDebug() << "Creating a backup from handler"; - QFile entitiesFile { _entitiesFilePath }; - qDebug() << entitiesFile.size(); if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 4cb42787ba..832fc8a3ff 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -154,7 +154,6 @@ void BackupSupervisor::loadBackup(QuaZip& zip) { } void BackupSupervisor::createBackup(QuaZip& zip) { - qDebug() << Q_FUNC_INFO; if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already an operation in progress."; return; diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 3f9b3f20d8..56571f1b8c 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -292,7 +292,6 @@ void DomainContentBackupManager::backup() { } for (auto& handler : _backupHandlers) { - qDebug() << "Backup handler"; handler.createBackup(zip); } From 69298246c4726a80aef6eda423024cc0a4ec4d22 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 15:44:51 -0800 Subject: [PATCH 012/157] CR --- domain-server/src/BackupHandler.h | 2 +- domain-server/src/BackupSupervisor.cpp | 32 ++++++++----------- domain-server/src/BackupSupervisor.h | 1 - .../src/DomainContentBackupManager.cpp | 9 ++++-- domain-server/src/DomainServer.cpp | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 332afe22f7..4643d183b2 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -1,6 +1,6 @@ // // BackupHandler.h -// assignment-client +// domain-server/src // // Created by Clement Brisset on 2/5/18. // Copyright 2018 High Fidelity, Inc. diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 832fc8a3ff..869f85c6cc 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -31,12 +31,11 @@ using namespace std; Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); -BackupSupervisor::BackupSupervisor(const QString& backupDirectory) { - _assetsDirectory = backupDirectory + ASSETS_DIR; - QDir assetsDir { _assetsDirectory }; - if (!assetsDir.exists()) { - assetsDir.mkpath("."); - } +BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : + _assetsDirectory(backupDirectory + ASSETS_DIR) +{ + // Make sure the asset directory exists. + QDir(_assetsDirectory).mkpath("."); refreshAssetsOnDisk(); @@ -166,7 +165,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + qCWarning(backup_supervisor) << "Backing up asset mappings that might be stale."; } AssetServerBackup backup; @@ -182,13 +181,13 @@ void BackupSupervisor::createBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { - qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.push_back(backup); @@ -207,7 +206,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that appear old."; + qCWarning(backup_supervisor) << "Current asset mappings that might be stale."; } startOperation(); @@ -216,7 +215,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup"; + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to restore."; stopOperation(); return; } @@ -237,7 +236,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup"; + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to delete."; return; } @@ -257,7 +256,7 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { return info.fileName() == zipInfo.fileName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName(); + qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to consolidate."; return; } @@ -274,13 +273,13 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; static const QString ZIP_ASSETS_FOLDER = "files/"; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { - qCDebug(backup_supervisor) << "testCreate(): outFile.open()"; + qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); continue; } zipFile.write(file.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); continue; } } @@ -294,9 +293,6 @@ void BackupSupervisor::refreshMappings() { QObject::connect(request, &GetAllMappingsRequest::finished, this, [this](GetAllMappingsRequest* request) { if (request->getError() == MappingRequest::NoError) { const auto& mappings = request->getMappings(); - - qCDebug(backup_supervisor) << "Refreshed" << mappings.size() << "asset mappings!"; - _currentMappings.clear(); for (const auto& mapping : mappings) { _currentMappings.insert({ mapping.first, mapping.second.hash }); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index d0f6e52ac6..9fedcca19b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -21,7 +21,6 @@ #include #include -#include #include diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 56571f1b8c..ed5d99f927 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -47,7 +47,11 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire bool debugTimestampNow) : _backupDirectory(backupDirectory), _persistInterval(persistInterval), - _lastCheck(usecTimestampNow()) { + _lastCheck(usecTimestampNow()) +{ + // Make sure the backup directory exists. + QDir(_backupDirectory).mkpath("."); + parseSettings(settings); } @@ -288,7 +292,8 @@ void DomainContentBackupManager::backup() { auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); if (!zip.open(QuaZip::mdAdd)) { - qDebug() << "Could not open archive"; + qDebug() << "Could not open backup archive:" << zip.getZipName(); + qDebug() << " ERROR:" << zip.getZipError(); } for (auto& handler : _backupHandlers) { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 06d3549ff8..8ccba3d942 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -295,7 +295,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : } maybeHandleReplacementEntityFile(); - _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.responseObjectForType("6")["entity_server_settings"].toObject())); + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); From e63b692d80f584eac5d70751b9e01d2a1a8b8cf9 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 7 Feb 2018 16:53:29 -0800 Subject: [PATCH 013/157] Add BackupHandler for entity file backups --- domain-server/src/BackupHandler.h | 34 ++++++++++++-- domain-server/src/BackupSupervisor.h | 47 +++++++++++++++++++ .../src/DomainContentBackupManager.cpp | 31 ++++++++++-- .../src/DomainContentBackupManager.h | 5 ++ domain-server/src/DomainServer.cpp | 2 + libraries/entities/src/EntityTree.cpp | 1 + 6 files changed, 113 insertions(+), 7 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 4643d183b2..b790591bea 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -83,7 +83,10 @@ public: void loadBackup(QuaZip& zip) {} - void createBackup(QuaZip& zip) const { + // Create a skeleton backup + void createBackup(QuaZip& zip) { + qDebug() << "Creating a backup from handler"; + QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { @@ -97,9 +100,32 @@ public: } } - void recoverBackup(QuaZip& zip) const {} - void deleteBackup(QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) const {} + // Recover from a full backup + void recoverBackup(QuaZip& zip) { + if (!zip.setCurrentFile("models.json.gz")) { + qWarning() << "Failed to find models.json.gz while recovering backup"; + return; + } + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::ReadOnly); + auto data = zipFile.readAll(); + + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::WriteOnly)) { + entitiesFile.write(data); + } + + zipFile.close(); + } + + // Delete a skeleton backup + void deleteBackup(QuaZip& zip) { + } + + // Create a full backup + void consolidateBackup(QuaZip& zip) { + } private: QString _entitiesFilePath; diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 9fedcca19b..1023622971 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -91,4 +91,51 @@ private: int _mappingRequestsInFlight { 0 }; }; + +#include +class AssetsBackupHandler { +public: + AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} + + void loadBackup(QuaZip& zip) {} + + void createBackup(QuaZip& zip) { + quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); + AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); + + if (lastRefreshTimestamp == 0) { + qWarning() << "Current mappings not yet loaded, "; + return; + } + + static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; + if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { + qWarning() << "Backing up asset mappings that appear old."; + } + + QJsonObject jsonObject; + for (const auto& mapping : mappings) { + jsonObject.insert(mapping.first, mapping.second); + } + QJsonDocument document(jsonObject); + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { + qDebug() << "testCreate(): outFile.open()"; + } + zipFile.write(document.toJson()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + } + } + + void recoverBackup(QuaZip& zip) {} + void deleteBackup(QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) {} + +private: + BackupSupervisor* _backupSupervisor; +}; + #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index ed5d99f927..b0a80531f8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -216,10 +216,35 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } +bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { + qDebug() << "Recoving from" << backupName; + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + if (backupFile.open(QIODevice::ReadOnly)) { + QuaZip zip { &backupFile }; + if (!zip.open(QuaZip::Mode::mdUnzip)) { + qWarning() << "Failed to unzip file: " << backupName; + backupFile.close(); + return false; + } + + for (auto& handler : _backupHandlers) { + handler.recoverBackup(zip); + } + + backupFile.close(); + } + + qDebug() << "Successfully recovered from " << backupName; + + return true; +} + void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { QDir backupDir { _backupDirectory }; if (backupDir.exists() && rule.maxBackupVersions > 0) { - qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name << "..."; + qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; auto matchingFiles = backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); @@ -235,11 +260,11 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } } - qCDebug(domain_server) << "Done rolling old backup versions..."; + qCDebug(domain_server) << "Done removing old backup versions"; } else { qCDebug(domain_server) << "Rolling backups for rule" << rule.name << "." << " Max Rolled Backup Versions less than 1 [" << rule.maxBackupVersions << "]." - << " No need to roll backups..."; + << " No need to roll backups"; } } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 69163b4ead..d0dd9cf2c6 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -46,6 +46,11 @@ public: void replaceData(QByteArray data); + bool recoverFromBackup(const QString& backupName); + +signals: + void loadCompleted(); + protected: /// Implements generic processing behavior for this thread. virtual void setup() override; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8ccba3d942..fe6a303e08 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -299,6 +299,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); + + _contentManager->recoverFromBackup("backup-daily_rolling-2018-02-06_15-13-50.zip"); } void DomainServer::parseCommandLine() { diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index f632bcf140..fc3e793b45 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2246,6 +2246,7 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer } entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; + qDebug() << "Writing to map: " << _persistDataVersion << _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); From 8b07e7e28ff8eea62fb60bf81c73bdb05c10fc36 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 8 Feb 2018 22:13:52 -0800 Subject: [PATCH 014/157] Add backup DS APIs Add backup apis --- domain-server/src/BackupHandler.h | 11 +- domain-server/src/BackupSupervisor.h | 47 ------ .../src/DomainContentBackupManager.cpp | 144 ++++++++++++++---- .../src/DomainContentBackupManager.h | 18 ++- domain-server/src/DomainServer.cpp | 72 ++++++++- .../embedded-webserver/src/HTTPConnection.cpp | 53 ++++++- .../embedded-webserver/src/HTTPConnection.h | 6 + .../embedded-webserver/src/HTTPManager.cpp | 15 +- 8 files changed, 270 insertions(+), 96 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index b790591bea..ad1fc6b793 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -95,7 +95,7 @@ public: zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } } @@ -107,7 +107,10 @@ public: return; } QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::ReadOnly); + if (!zipFile.open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open models.json.gz in backup"; + return; + } auto data = zipFile.readAll(); QFile entitiesFile { _entitiesFilePath }; @@ -117,6 +120,10 @@ public: } zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + } } // Delete a skeleton backup diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 1023622971..9fedcca19b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -91,51 +91,4 @@ private: int _mappingRequestsInFlight { 0 }; }; - -#include -class AssetsBackupHandler { -public: - AssetsBackupHandler(BackupSupervisor* backupSupervisor) : _backupSupervisor(backupSupervisor) {} - - void loadBackup(QuaZip& zip) {} - - void createBackup(QuaZip& zip) { - quint64 lastRefreshTimestamp = _backupSupervisor->getLastRefreshTimestamp(); - AssetUtils::Mappings mappings = _backupSupervisor->getCurrentMappings(); - - if (lastRefreshTimestamp == 0) { - qWarning() << "Current mappings not yet loaded, "; - return; - } - - static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; - if (usecTimestampNow() - lastRefreshTimestamp > MAX_REFRESH_TIME) { - qWarning() << "Backing up asset mappings that appear old."; - } - - QJsonObject jsonObject; - for (const auto& mapping : mappings) { - jsonObject.insert(mapping.first, mapping.second); - } - QJsonDocument document(jsonObject); - - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("mappings.json"))) { - qDebug() << "testCreate(): outFile.open()"; - } - zipFile.write(document.toJson()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "testCreate(): outFile.close(): " << zipFile.getZipError(); - } - } - - void recoverBackup(QuaZip& zip) {} - void deleteBackup(QuaZip& zip) {} - void consolidateBackup(QuaZip& zip) {} - -private: - BackupSupervisor* _backupSupervisor; -}; - #endif /* hifi_BackupSupervisor_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index b0a80531f8..29f6b7948f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "DomainServer.h" #include "DomainContentBackupManager.h" @@ -36,7 +37,8 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // // Backup format looks like: daily_backup-TIMESTAMP.zip const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); - +static const QString AUTOMATIC_BACKUP_PREFIX{ "autobackup-" }; +static const QString MANUAL_BACKUP_PREFIX{ "backup-" }; void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { _backupHandlers.push_back(std::move(handler)); } @@ -83,7 +85,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { auto name = obj["Name"].toString(); auto format = obj["format"].toString(); - format = name.replace(" ", "_").toLower() + "-"; + format = name.replace(" ", "_").toLower(); qCDebug(domain_server) << " Name:" << name; qCDebug(domain_server) << " format:" << format; @@ -129,6 +131,14 @@ void DomainContentBackupManager::setup() { } bool DomainContentBackupManager::process() { + if (!_initialLoadComplete) { + QDir backupDir { _backupDirectory }; + if (!backupDir.exists()) { + backupDir.mkpath("."); + } + _initialLoadComplete = true; + } + if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms @@ -140,7 +150,7 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; - persist(); + backup(); } } @@ -149,32 +159,18 @@ bool DomainContentBackupManager::process() { void DomainContentBackupManager::aboutToFinish() { qCDebug(domain_server) << "Persist thread about to finish..."; - persist(); -} - -void DomainContentBackupManager::persist() { - QDir backupDir { _backupDirectory }; - backupDir.mkpath("."); - - // create our "lock" file to indicate we're saving. - QString lockFileName = _backupDirectory + "/running.lock"; - - std::ofstream lockFile(qPrintable(lockFileName), std::ios::out | std::ios::binary); - if (lockFile.is_open()) { - backup(); - - lockFile.close(); - remove(qPrintable(lockFileName)); - } + backup(); + qCDebug(domain_server) << "Persist thread done with about to finish..."; + _stopThread = true; } bool DomainContentBackupManager::getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime) { - QRegExp formatRE { QRegExp::escape(format) + "(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; + QRegExp formatRE { AUTOMATIC_BACKUP_PREFIX + QRegExp::escape(format) + "\\-(" + DATETIME_FORMAT_RE + ")" + "\\.zip" }; QStringList filters; - filters << format + "*.zip"; + filters << AUTOMATIC_BACKUP_PREFIX + format + "*.zip"; bool bestBackupFound = false; QString bestBackupFile; @@ -216,7 +212,32 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } +bool DomainContentBackupManager::deleteBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "deleteBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + + QDir backupDir { _backupDirectory }; + QFile backupFile { backupDir.filePath(backupName) }; + if (backupFile.remove()) { + return true; + } + return false; +} + bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { + if (QThread::currentThread() != thread()) { + bool result{ false }; + BLOCKING_INVOKE_METHOD(this, "recoverFromBackup", + Q_RETURN_ARG(bool, result), + Q_ARG(const QString&, backupName)); + return result; + } + qDebug() << "Recoving from" << backupName; QDir backupDir { _backupDirectory }; @@ -226,7 +247,6 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { if (!zip.open(QuaZip::Mode::mdUnzip)) { qWarning() << "Failed to unzip file: " << backupName; backupFile.close(); - return false; } for (auto& handler : _backupHandlers) { @@ -234,11 +254,43 @@ bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { } backupFile.close(); + qDebug() << "Successfully recovered from " << backupName; + return true; + } else { + qWarning() << "Invalid id: " << backupName; + return false; + } +} + +std::vector DomainContentBackupManager::getAllBackups() { + std::vector backups; + + QDir backupDir { _backupDirectory }; + auto matchingFiles = + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, + QDir::Files | QDir::NoSymLinks, QDir::Name); + QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")"; + QString nameFormat = "(.+)"; + QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; + QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + + for (const auto& fileInfo : matchingFiles) { + auto fileName = fileInfo.fileName(); + if (backupNameFormat.exactMatch(fileName)) { + auto type = backupNameFormat.cap(1); + auto name = backupNameFormat.cap(2); + auto dateTime = backupNameFormat.cap(3); + auto createdAt = QDateTime::fromString(dateTime, DATETIME_FORMAT); + if (!createdAt.isValid()) { + continue; + } + + BackupItemInfo backup { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }; + backups.push_back(backup); + } } - qDebug() << "Successfully recovered from " << backupName; - - return true; + return backups; } void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { @@ -247,9 +299,10 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) qCDebug(domain_server) << "Rolling old backup versions for rule" << rule.name; auto matchingFiles = - backupDir.entryInfoList({ rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)"; for (int i = 0; i < backupsToDelete; ++i) { auto fileInfo = matchingFiles[i].absoluteFilePath(); QFile backupFile(fileInfo); @@ -313,6 +366,7 @@ void DomainContentBackupManager::backup() { qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; +<<<<<<< HEAD auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; QuaZip zip(_backupDirectory + "/" + fileName); @@ -323,11 +377,17 @@ void DomainContentBackupManager::backup() { for (auto& handler : _backupHandlers) { handler.createBackup(zip); +======= + bool success; + QString path; + std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, rule.extensionFormat); + if (!success) { + qCWarning(domain_server) << "Failed to create backup for" << rule.name << "at" << path; + continue; +>>>>>>> dd86471a42... Add backup DS APIs } - zip.close(); - - qDebug() << "Created backup: " << fileName; + qDebug() << "Created backup: " << path; rule.lastBackupSeconds = nowSeconds; @@ -365,3 +425,27 @@ void DomainContentBackupManager::consolidate(QString fileName) { zip.close(); } } + +void DomainContentBackupManager::createManualBackup(const QString& name) { + createBackup(MANUAL_BACKUP_PREFIX, name); +} + +std::pair DomainContentBackupManager::createBackup(const QString& prefix, const QString& name) { + auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); + auto fileName = prefix + name + "-" + timestamp + ".zip"; + auto path = _backupDirectory + "/" + fileName; + QuaZip zip(path); + if (!zip.open(QuaZip::mdAdd)) { + qCWarning(domain_server) << "Failed to open zip file at " << path; + qCWarning(domain_server) << " ERROR:" << zip.getZipError(); + return { false, path }; + } + + for (auto& handler : _backupHandlers) { + handler.createBackup(zip); + } + + zip.close(); + + return { true, path }; +} diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index d0dd9cf2c6..461d4dd794 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -21,6 +21,14 @@ #include "BackupHandler.h" +struct BackupItemInfo { + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -41,12 +49,18 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); + bool isInitialLoadComplete() const { return _initialLoadComplete; } + std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); + void createManualBackup(const QString& name); + +public slots: bool recoverFromBackup(const QString& backupName); + bool deleteBackup(const QString& backupName); signals: void loadCompleted(); @@ -56,7 +70,6 @@ protected: virtual void setup() override; virtual bool process() override; - void persist(); void load(); void backup(); void consolidate(QString fileName); @@ -65,10 +78,13 @@ protected: int64_t getMostRecentBackupTimeInSecs(const QString& format); void parseSettings(const QJsonObject& settings); + std::pair createBackup(const QString& prefix, const QString& name); + private: QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; + bool _initialLoadComplete { false }; int64_t _lastCheck { 0 }; std::vector _backupRules; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fe6a303e08..1949c40566 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -300,7 +300,10 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); - _contentManager->recoverFromBackup("backup-daily_rolling-2018-02-06_15-13-50.zip"); + qDebug() << "Existing backups:"; + for (auto& backup : _contentManager->getAllBackups()) { + qDebug() << " Backup: " << backup.name << backup.createdAt; + } } void DomainServer::parseCommandLine() { @@ -1736,6 +1739,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerreadAll(); auto filePath = getEntitiesFilePath(); + QDir dir(getEntitiesDirPath()); + if (!dir.exists()) { + qCDebug(domain_server) << "Creating entities content directory:" << dir.absolutePath(); + dir.mkpath("."); + } + QFile f(filePath); if (f.open(QIODevice::WriteOnly)) { f.write(data); @@ -1746,12 +1755,12 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointerrespond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); + return true; + } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); + _contentManager->recoverFromBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = true; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + } else if (url.path() == URI_API_BACKUPS) { + QJsonObject rootJSON; + QJsonArray backupsJSON; + + auto backups = _contentManager->getAllBackups(); + + for (const auto& backup : backups) { + QJsonObject obj; + obj["id"] = backup.id; + obj["name"] = backup.name; + obj["createdAtMillis"] = backup.createdAt.toMSecsSinceEpoch(); + obj["isManualBackup"] = backup.isManualBackup; + backupsJSON.push_back(obj); + } + + rootJSON["backups"] = backupsJSON; + QJsonDocument docJSON(rootJSON); + + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); return true; } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); @@ -2213,6 +2254,20 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; + } else if (url.path() == URI_API_BACKUPS) { + qDebug() << "GOt request to create a backup:"; + auto params = connection->parseUrlEncodedForm(); + auto it = params.find("name"); + if (it == params.end()) { + connection->respond(HTTPConnection::StatusCode400, "Bad request, missing `name`"); + return true; + } + + _contentManager->createManualBackup(it.value()); + + connection->respond(HTTPConnection::StatusCode200); + return true; + } else if (url.path() == "/domain_settings") { auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); if (!accessTokenVariant) { @@ -2311,7 +2366,16 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QRegExp allNodesDeleteRegex(ALL_NODE_DELETE_REGEX_STRING); QRegExp nodeDeleteRegex(NODE_DELETE_REGEX_STRING); - if (nodeDeleteRegex.indexIn(url.path()) != -1) { + if (url.path().startsWith(URI_API_BACKUPS_ID)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); + auto success = _contentManager->deleteBackup(id); + QJsonObject rootJSON; + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + + } else if (nodeDeleteRegex.indexIn(url.path()) != -1) { // this is a request to DELETE one node by UUID // pull the captured string, if it exists diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index a61bc95f8b..6496cc3f68 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,12 +133,33 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { + QByteArray data(content); + auto device { std::unique_ptr(new QBuffer()) }; + device->setBuffer(new QByteArray(content)); + if (device->open(QIODevice::ReadOnly)) { + respond(code, std::move(device), contentType, headers); + } else { + qCritical() << "Error opening QBuffer to respond to " << _requestUrl.path(); + } +} + +void HTTPConnection::respond(const char* code, std::unique_ptr device, const char* contentType, const Headers& headers) { + _responseDevice = std::move(device); + _socket->write("HTTP/1.1 "); + + if (_responseDevice->isSequential()) { + qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; + _socket->write(StatusCode500); + _socket->write("\r\n"); + _socket->disconnect(SIGNAL(readyRead()), this); + _socket->disconnectFromHost(); + return; + } + _socket->write(code); _socket->write("\r\n"); - int csize = content.size(); - for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); it != end; it++) { _socket->write(it.key()); @@ -146,6 +167,8 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const _socket->write(it.value()); _socket->write("\r\n"); } + + int csize = _responseDevice->size(); if (csize > 0) { _socket->write("Content-Length: "); _socket->write(QByteArray::number(csize)); @@ -157,20 +180,35 @@ void HTTPConnection::respond(const char* code, const QByteArray& content, const } _socket->write("Connection: close\r\n\r\n"); - if (csize > 0) { - _socket->write(content); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + } else { + constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; + int totalToBeWritten = csize; + connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { + if (!_responseDevice->atEnd()) { + totalToBeWritten -= _socket->write(_responseDevice->read(HTTP_RESPONSE_CHUNK_SIZE)); + if (_responseDevice->atEnd()) { + _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::bytesWritten, this, nullptr); + } + } + }); + } // make sure we receive no further read notifications - _socket->disconnect(SIGNAL(readyRead()), this); - - _socket->disconnectFromHost(); + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } void HTTPConnection::readRequest() { if (!_socket->canReadLine()) { return; } + if (!_requestUrl.isEmpty()) { + qDebug() << "Request URL was already set"; + return; + } // parse out the method and resource QByteArray line = _socket->readLine().trimmed(); if (line.startsWith("HEAD")) { @@ -249,6 +287,7 @@ void HTTPConnection::readContent() { if (_socket->bytesAvailable() < size) { return; } + qDebug() << "Reading content"; _socket->read(_requestContent.data(), size); _socket->disconnect(this, SLOT(readContent())); diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 966fc26949..9c435b14a0 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -87,6 +87,9 @@ public: void respond (const char* code, const QByteArray& content = QByteArray(), const char* contentType = DefaultContentType, const Headers& headers = Headers()); + void respond (const char* code, std::unique_ptr device, + const char* contentType = DefaultContentType, + const Headers& headers = Headers()); protected slots: @@ -127,6 +130,9 @@ protected: /// The content of the request. QByteArray _requestContent; + + /// Response content + std::unique_ptr _responseDevice; }; #endif // hifi_HTTPConnection_h diff --git a/libraries/embedded-webserver/src/HTTPManager.cpp b/libraries/embedded-webserver/src/HTTPManager.cpp index fd127a2e92..bd1b545412 100644 --- a/libraries/embedded-webserver/src/HTTPManager.cpp +++ b/libraries/embedded-webserver/src/HTTPManager.cpp @@ -98,13 +98,14 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, // file exists, serve it static QMimeDatabase mimeDatabase; - QFile localFile(filePath); - localFile.open(QIODevice::ReadOnly); - QByteArray localFileData = localFile.readAll(); + auto localFile = std::unique_ptr(new QFile(filePath)); + localFile->open(QIODevice::ReadOnly); + QByteArray localFileData; QFileInfo localFileInfo(filePath); if (localFileInfo.completeSuffix() == "shtml") { + localFileData = localFile->readAll(); // this is a file that may have some SSI statements // the only thing we support is the include directive, but check the contents for that @@ -153,8 +154,12 @@ bool HTTPManager::handleHTTPRequest(HTTPConnection* connection, const QUrl& url, ? QString { "text/html" } : mimeDatabase.mimeTypeForFile(filePath).name(); - connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); - + if (localFileData.isNull()) { + connection->respond(HTTPConnection::StatusCode200, std::move(localFile), qPrintable(mimeType)); + } else { + connection->respond(HTTPConnection::StatusCode200, localFileData, qPrintable(mimeType)); + } + return true; } } From dd398da2e0baa1306ce754edd73a70e49144be6e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 15:36:50 -0800 Subject: [PATCH 015/157] Update DS to use promises for backup APIs --- .../src/DomainContentBackupManager.cpp | 50 ++++++++++--------- .../src/DomainContentBackupManager.h | 6 ++- domain-server/src/DomainServer.cpp | 29 ++++++----- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 29f6b7948f..5c4d70d7ad 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -212,54 +212,58 @@ bool DomainContentBackupManager::getMostRecentBackup(const QString& format, return bestBackupFound; } -bool DomainContentBackupManager::deleteBackup(const QString& backupName) { +void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, const QString& backupName) { if (QThread::currentThread() != thread()) { - bool result{ false }; - BLOCKING_INVOKE_METHOD(this, "deleteBackup", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, backupName)); - return result; + QMetaObject::invokeMethod(this, "deleteBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; } + bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.remove()) { - return true; + success = true; } - return false; + promise->resolve({ + { "success", success } + }); } -bool DomainContentBackupManager::recoverFromBackup(const QString& backupName) { +void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { if (QThread::currentThread() != thread()) { - bool result{ false }; - BLOCKING_INVOKE_METHOD(this, "recoverFromBackup", - Q_RETURN_ARG(bool, result), - Q_ARG(const QString&, backupName)); - return result; + QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, backupName)); + return; } qDebug() << "Recoving from" << backupName; + bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.open(QIODevice::ReadOnly)) { QuaZip zip { &backupFile }; if (!zip.open(QuaZip::Mode::mdUnzip)) { qWarning() << "Failed to unzip file: " << backupName; - backupFile.close(); + success = false; + } else { + for (auto& handler : _backupHandlers) { + handler.recoverBackup(zip); + } + + qDebug() << "Successfully recovered from " << backupName; + success = true; } - - for (auto& handler : _backupHandlers) { - handler.recoverBackup(zip); - } - backupFile.close(); - qDebug() << "Successfully recovered from " << backupName; - return true; } else { + success = false; qWarning() << "Invalid id: " << backupName; - return false; } + + promise->resolve({ + { "success", success } + }); } std::vector DomainContentBackupManager::getAllBackups() { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 461d4dd794..6d6f07a19e 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -21,6 +21,8 @@ #include "BackupHandler.h" +#include + struct BackupItemInfo { QString id; QString name; @@ -59,8 +61,8 @@ public: void createManualBackup(const QString& name); public slots: - bool recoverFromBackup(const QString& backupName); - bool deleteBackup(const QString& backupName); + void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); + void deleteBackup(MiniPromise::Promise promise, const QString& backupName); signals: void loadCompleted(); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 1949c40566..0e057972ca 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2124,11 +2124,14 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); - _contentManager->recoverFromBackup(id); - QJsonObject rootJSON; - rootJSON["success"] = true; - QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + auto deferred = makePromise("recoverFromBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + rootJSON["success"] = result["success"].toBool(); + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->recoverFromBackup(deferred, id); return true; } else if (url.path() == URI_API_BACKUPS) { QJsonObject rootJSON; @@ -2368,11 +2371,15 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (url.path().startsWith(URI_API_BACKUPS_ID)) { auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); - auto success = _contentManager->deleteBackup(id); - QJsonObject rootJSON; - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + auto deferred = makePromise("deleteBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + rootJSON["success"] = result["success"].toBool(); + QJsonDocument docJSON(rootJSON); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->deleteBackup(deferred, id); + return true; } else if (nodeDeleteRegex.indexIn(url.path()) != -1) { @@ -3310,8 +3317,6 @@ void DomainServer::maybeHandleReplacementEntityFile() { } void DomainServer::handleOctreeFileReplacement(QByteArray octreeFile) { - // enumerate the nodes and find any octree type servers with active sockets - //Assume we have compressed data auto compressedOctree = octreeFile; QByteArray jsonOctree; From 80b03b904610911577d58d21d4d59de0cc93b39a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 16:09:16 -0800 Subject: [PATCH 016/157] Make backup directory in content manager const --- domain-server/src/DomainContentBackupManager.cpp | 13 ------------- domain-server/src/DomainContentBackupManager.h | 2 +- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 5c4d70d7ad..159d6d6e95 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -370,25 +370,12 @@ void DomainContentBackupManager::backup() { qCDebug(domain_server) << "Time since last backup [" << secondsSinceLastBackup << "] for rule [" << rule.name << "] exceeds backup interval [" << rule.intervalSeconds << "] doing backup now..."; -<<<<<<< HEAD - auto timestamp = QDateTime::currentDateTime().toString(DATETIME_FORMAT); - auto fileName = "backup-" + rule.extensionFormat + timestamp + ".zip"; - QuaZip zip(_backupDirectory + "/" + fileName); - if (!zip.open(QuaZip::mdAdd)) { - qDebug() << "Could not open backup archive:" << zip.getZipName(); - qDebug() << " ERROR:" << zip.getZipError(); - } - - for (auto& handler : _backupHandlers) { - handler.createBackup(zip); -======= bool success; QString path; std::tie(success, path) = createBackup(AUTOMATIC_BACKUP_PREFIX, rule.extensionFormat); if (!success) { qCWarning(domain_server) << "Failed to create backup for" << rule.name << "at" << path; continue; ->>>>>>> dd86471a42... Add backup DS APIs } qDebug() << "Created backup: " << path; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 6d6f07a19e..792695acce 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -83,7 +83,7 @@ protected: std::pair createBackup(const QString& prefix, const QString& name); private: - QString _backupDirectory; + const QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; bool _initialLoadComplete { false }; From 8a69c69bec02016ebdea92ea19e50d15cf9e1b74 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 16:49:23 -0800 Subject: [PATCH 017/157] CR --- domain-server/src/BackupHandler.h | 15 ++++++------ .../src/DomainContentBackupManager.cpp | 23 +++++-------------- .../src/DomainContentBackupManager.h | 2 -- domain-server/src/DomainServer.cpp | 16 +++++++------ .../embedded-webserver/src/HTTPConnection.cpp | 1 - libraries/entities/src/EntityTree.cpp | 1 - 6 files changed, 23 insertions(+), 35 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index ad1fc6b793..3e25af83e8 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -79,14 +79,14 @@ private: #include class EntitiesBackupHandler { public: - EntitiesBackupHandler(QString entitiesFilePath) : _entitiesFilePath(entitiesFilePath) {} + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) + : _entitiesFilePath(entitiesFilePath) + , _entitiesReplacementFilePath {} void loadBackup(QuaZip& zip) {} // Create a skeleton backup void createBackup(QuaZip& zip) { - qDebug() << "Creating a backup from handler"; - QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { @@ -95,7 +95,7 @@ public: zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } } @@ -108,12 +108,12 @@ public: } QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open models.json.gz in backup"; + qCritical() << "Failed to open models.json.gz in backup"; return; } auto data = zipFile.readAll(); - QFile entitiesFile { _entitiesFilePath }; + QFile entitiesFile { _entitiesReplacementFilePath }; if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data); @@ -122,7 +122,7 @@ public: zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qDebug() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); } } @@ -136,6 +136,7 @@ public: private: QString _entitiesFilePath; + QString _entitiesReplacementFilePath; }; #endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 159d6d6e95..6f311613d5 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -35,10 +35,10 @@ const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds // Backup format looks like: daily_backup-TIMESTAMP.zip -const static QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; -const static QString DATETIME_FORMAT_RE("\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}"); -static const QString AUTOMATIC_BACKUP_PREFIX{ "autobackup-" }; -static const QString MANUAL_BACKUP_PREFIX{ "backup-" }; +static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; +static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" }; +static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" }; +static const QString MANUAL_BACKUP_PREFIX { "backup-" }; void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { _backupHandlers.push_back(std::move(handler)); } @@ -131,14 +131,6 @@ void DomainContentBackupManager::setup() { } bool DomainContentBackupManager::process() { - if (!_initialLoadComplete) { - QDir backupDir { _backupDirectory }; - if (!backupDir.exists()) { - backupDir.mkpath("."); - } - _initialLoadComplete = true; - } - if (isStillRunning()) { constexpr int64_t MSECS_TO_USECS = 1000; constexpr int64_t USECS_TO_SLEEP = 10 * MSECS_TO_USECS; // every 10ms @@ -219,12 +211,9 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons return; } - bool success { false }; QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; - if (backupFile.remove()) { - success = true; - } + auto success = backupFile.remove(); promise->resolve({ { "success", success } }); @@ -237,7 +226,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, return; } - qDebug() << "Recoving from" << backupName; + qDebug() << "Recovering from" << backupName; bool success { false }; QDir backupDir { _backupDirectory }; diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 792695acce..cfeae9c8b9 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -51,7 +51,6 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandler handler); - bool isInitialLoadComplete() const { return _initialLoadComplete; } std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -86,7 +85,6 @@ private: const QString _backupDirectory; std::vector _backupHandlers; int _persistInterval { 0 }; - bool _initialLoadComplete { false }; int64_t _lastCheck { 0 }; std::vector _backupRules; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 0e057972ca..718c5ff402 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,7 +296,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath())); + _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())); _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); _contentManager->initialize(true); @@ -1936,7 +1936,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_API_BACKUPS = "/api/backups"; const QString URI_API_BACKUPS_ID = "/api/backups/"; const QString URI_API_BACKUPS_RECOVER = "/api/backups/recover/"; - //const QString URI_API_BACKUPS_CREATE = "/api/backups"; const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"; @@ -2127,9 +2126,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto deferred = makePromise("recoverFromBackup"); deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { QJsonObject rootJSON; - rootJSON["success"] = result["success"].toBool(); + auto success = result["success"].toBool(); + rootJSON["success"] = success; QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); }); _contentManager->recoverFromBackup(deferred, id); return true; @@ -2258,7 +2259,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - qDebug() << "GOt request to create a backup:"; auto params = connection->parseUrlEncodedForm(); auto it = params.find("name"); if (it == params.end()) { @@ -2374,9 +2374,11 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto deferred = makePromise("deleteBackup"); deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { QJsonObject rootJSON; - rootJSON["success"] = result["success"].toBool(); + auto success = result["success"].toBool(); + rootJSON["success"] = success; QJsonDocument docJSON(rootJSON); - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); }); _contentManager->deleteBackup(deferred, id); diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 6496cc3f68..1368a9f54c 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -287,7 +287,6 @@ void HTTPConnection::readContent() { if (_socket->bytesAvailable() < size) { return; } - qDebug() << "Reading content"; _socket->read(_requestContent.data(), size); _socket->disconnect(this, SLOT(readContent())); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index fc3e793b45..f632bcf140 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -2246,7 +2246,6 @@ bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer } entityDescription["DataVersion"] = _persistDataVersion; entityDescription["Id"] = _persistID; - qDebug() << "Writing to map: " << _persistDataVersion << _persistID; QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents, _myAvatar); From b6240e8622c8591ee57972abc5a2c719b608395b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 14 Feb 2018 17:02:11 -0800 Subject: [PATCH 018/157] Move backup recover API to POST --- domain-server/src/DomainServer.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 718c5ff402..416c8e39b6 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2120,19 +2120,6 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url // send the response connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE)); - return true; - } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { - auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); - auto deferred = makePromise("recoverFromBackup"); - deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { - QJsonObject rootJSON; - auto success = result["success"].toBool(); - rootJSON["success"] = success; - QJsonDocument docJSON(rootJSON); - connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), - JSON_MIME_TYPE.toUtf8()); - }); - _contentManager->recoverFromBackup(deferred, id); return true; } else if (url.path() == URI_API_BACKUPS) { QJsonObject rootJSON; @@ -2279,8 +2266,21 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url } } else if (url.path() == URI_API_DOMAINS) { - return forwardMetaverseAPIRequest(connection, "/api/v1/domains", "domain", { "label" }); + + } else if (url.path().startsWith(URI_API_BACKUPS_RECOVER)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_RECOVER).length()); + auto deferred = makePromise("recoverFromBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->recoverFromBackup(deferred, id); + return true; } } else if (connection->requestOperation() == QNetworkAccessManager::PutOperation) { if (url.path() == URI_API_DOMAINS) { From f2b6823748cf0c257aa421a3cbf1718c65788245 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 08:20:19 -0800 Subject: [PATCH 019/157] Fix initializer in EntitiesBackupHandler --- domain-server/src/BackupHandler.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 3e25af83e8..7d1a0bdb24 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -81,7 +81,7 @@ class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : _entitiesFilePath(entitiesFilePath) - , _entitiesReplacementFilePath {} + , _entitiesReplacementFilePath(entitiesReplacementFilePath) {} void loadBackup(QuaZip& zip) {} From 2cfa91be0688ead85dd702b6e43f1fd534727900 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 08:48:43 -0800 Subject: [PATCH 020/157] Add memory include to HTTPConnection --- libraries/embedded-webserver/src/HTTPConnection.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index 9c435b14a0..a020dfdca9 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -26,6 +26,8 @@ #include #include +#include + class QTcpSocket; class HTTPManager; class MaskFilter; From b832e118cc0adec0392949fcfee1b880de76af45 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:14:20 -0800 Subject: [PATCH 021/157] Add 'override' to BackupHandler methods --- domain-server/src/BackupHandler.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 7d1a0bdb24..045fcedc71 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -54,19 +54,19 @@ private: struct Model : Concept { Model(T* x) : data(x) {} - void loadBackup(QuaZip& zip) { + void loadBackup(QuaZip& zip) override { data->loadBackup(zip); } - void createBackup(QuaZip& zip) { + void createBackup(QuaZip& zip) override { data->createBackup(zip); } - void recoverBackup(QuaZip& zip) { + void recoverBackup(QuaZip& zip) override { data->recoverBackup(zip); } - void deleteBackup(QuaZip& zip) { + void deleteBackup(QuaZip& zip) override { data->deleteBackup(zip); } - void consolidateBackup(QuaZip& zip) { + void consolidateBackup(QuaZip& zip) override { data->consolidateBackup(zip); } From 145a8b385b7432dc906c9f1faefd9f12ebbd4435 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:36:59 -0800 Subject: [PATCH 022/157] Move HTTP_RESPONSE_CHUNK_SIZE into lambda for http response --- libraries/embedded-webserver/src/HTTPConnection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 1368a9f54c..6d0126b3d1 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -183,9 +183,9 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device if (_responseDevice->atEnd()) { _socket->disconnectFromHost(); } else { - constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; int totalToBeWritten = csize; connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { + constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; if (!_responseDevice->atEnd()) { totalToBeWritten -= _socket->write(_responseDevice->read(HTTP_RESPONSE_CHUNK_SIZE)); if (_responseDevice->atEnd()) { From 1aba89b908cc29565b600beff5d4539f27496d19 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 09:38:47 -0800 Subject: [PATCH 023/157] Fix style of init list --- domain-server/src/BackupHandler.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 045fcedc71..5fea3be6af 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -79,9 +79,9 @@ private: #include class EntitiesBackupHandler { public: - EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) - : _entitiesFilePath(entitiesFilePath) - , _entitiesReplacementFilePath(entitiesReplacementFilePath) {} + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : + _entitiesFilePath(entitiesFilePath), + _entitiesReplacementFilePath(entitiesReplacementFilePath) {} void loadBackup(QuaZip& zip) {} From 4b2e907ada0ce5b64606d9fbf2e4c42b793e4d0c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:14 -0800 Subject: [PATCH 024/157] Update entities recover backup to reset id and version --- domain-server/src/BackupHandler.h | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 5fea3be6af..f2735e5adf 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -77,6 +77,8 @@ private: }; #include +#include + class EntitiesBackupHandler { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -111,18 +113,27 @@ public: qCritical() << "Failed to open models.json.gz in backup"; return; } - auto data = zipFile.readAll(); + auto rawData = zipFile.readAll(); + + zipFile.close(); + + OctreeUtils::RawOctreeData data; + if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { + qCritical() << "Unable to parse octree data during backup recovery"; + return; + } + + data.resetIdAndVersion(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); + return; + } QFile entitiesFile { _entitiesReplacementFilePath }; if (entitiesFile.open(QIODevice::WriteOnly)) { - entitiesFile.write(data); - } - - zipFile.close(); - - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + entitiesFile.write(data.toGzippedByteArray()); } } From df809f5a3eb2b80bc73f39789554bf60dde3332a Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:29 -0800 Subject: [PATCH 025/157] Cleanup logging for backup cleanup --- .../src/DomainContentBackupManager.cpp | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 6f311613d5..347923a282 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -295,18 +295,21 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + rule.extensionFormat + "*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); int backupsToDelete = matchingFiles.length() - rule.maxBackupVersions; - qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)"; - for (int i = 0; i < backupsToDelete; ++i) { - auto fileInfo = matchingFiles[i].absoluteFilePath(); - QFile backupFile(fileInfo); - if (backupFile.remove()) { - qCDebug(domain_server) << "Removed old backup: " << backupFile.fileName(); - } else { - qCDebug(domain_server) << "Failed to remove old backup: " << backupFile.fileName(); + if (backupsToDelete <= 0) { + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, no backups need to be deleted"; + } else { + qCDebug(domain_server) << "Found" << matchingFiles.length() << "backups, deleting " << backupsToDelete << "backup(s)"; + for (int i = 0; i < backupsToDelete; ++i) { + auto fileInfo = matchingFiles[i].absoluteFilePath(); + QFile backupFile(fileInfo); + if (backupFile.remove()) { + qCDebug(domain_server) << "Removed old backup: " << backupFile.fileName(); + } else { + qCDebug(domain_server) << "Failed to remove old backup: " << backupFile.fileName(); + } } + qCDebug(domain_server) << "Done removing old backup versions"; } - - qCDebug(domain_server) << "Done removing old backup versions"; } else { qCDebug(domain_server) << "Rolling backups for rule" << rule.name << "." << " Max Rolled Backup Versions less than 1 [" << rule.maxBackupVersions << "]." From efb2473fcf19737f63032331a632f8cca9672337 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:02:55 -0800 Subject: [PATCH 026/157] Updaet createManualBackup to defer response until creation is done --- domain-server/src/DomainContentBackupManager.cpp | 16 ++++++++++++++-- domain-server/src/DomainContentBackupManager.h | 3 +-- domain-server/src/DomainServer.cpp | 12 ++++++++++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 347923a282..66655ea966 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -409,8 +409,20 @@ void DomainContentBackupManager::consolidate(QString fileName) { } } -void DomainContentBackupManager::createManualBackup(const QString& name) { - createBackup(MANUAL_BACKUP_PREFIX, name); +void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "createManualBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, name)); + return; + } + + bool success; + QString path; + std::tie(success, path) = createBackup(MANUAL_BACKUP_PREFIX, name); + + promise->resolve({ + { "success", success } + }); } std::pair DomainContentBackupManager::createBackup(const QString& prefix, const QString& name) { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index cfeae9c8b9..5cf8d4698f 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -57,9 +57,8 @@ public: void replaceData(QByteArray data); - void createManualBackup(const QString& name); - public slots: + void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 416c8e39b6..da8527bf16 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2253,9 +2253,17 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } - _contentManager->createManualBackup(it.value()); + auto deferred = makePromise("createManualBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->createManualBackup(deferred, it.value()); - connection->respond(HTTPConnection::StatusCode200); return true; } else if (url.path() == "/domain_settings") { From ce93b9a1f4ea001587a0d8ecc39d4473326e1cf8 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:48:37 -0800 Subject: [PATCH 027/157] Simplify BackupHandler pattern --- domain-server/src/BackupHandler.h | 64 +++---------------- domain-server/src/BackupSupervisor.h | 4 +- .../src/DomainContentBackupManager.cpp | 11 ++-- .../src/DomainContentBackupManager.h | 4 +- domain-server/src/DomainServer.cpp | 4 +- 5 files changed, 22 insertions(+), 65 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index f2735e5adf..eb9c35f236 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -18,68 +18,22 @@ #include -class BackupHandler { +class BackupHandlerInterface { public: - template - BackupHandler(T* x) : _self(new Model(x)) {} + virtual ~BackupHandlerInterface() = default; - void loadBackup(QuaZip& zip) { - _self->loadBackup(zip); - } - void createBackup(QuaZip& zip) { - _self->createBackup(zip); - } - void recoverBackup(QuaZip& zip) { - _self->recoverBackup(zip); - } - void deleteBackup(QuaZip& zip) { - _self->deleteBackup(zip); - } - void consolidateBackup(QuaZip& zip) { - _self->consolidateBackup(zip); - } - -private: - struct Concept { - virtual ~Concept() = default; - - virtual void loadBackup(QuaZip& zip) = 0; - virtual void createBackup(QuaZip& zip) = 0; - virtual void recoverBackup(QuaZip& zip) = 0; - virtual void deleteBackup(QuaZip& zip) = 0; - virtual void consolidateBackup(QuaZip& zip) = 0; - }; - - template - struct Model : Concept { - Model(T* x) : data(x) {} - - void loadBackup(QuaZip& zip) override { - data->loadBackup(zip); - } - void createBackup(QuaZip& zip) override { - data->createBackup(zip); - } - void recoverBackup(QuaZip& zip) override { - data->recoverBackup(zip); - } - void deleteBackup(QuaZip& zip) override { - data->deleteBackup(zip); - } - void consolidateBackup(QuaZip& zip) override { - data->consolidateBackup(zip); - } - - std::unique_ptr data; - }; - - std::unique_ptr _self; + virtual void loadBackup(QuaZip& zip) = 0; + virtual void createBackup(QuaZip& zip) = 0; + virtual void recoverBackup(QuaZip& zip) = 0; + virtual void deleteBackup(QuaZip& zip) = 0; + virtual void consolidateBackup(QuaZip& zip) = 0; }; +using BackupHandlerPointer = std::unique_ptr; #include #include -class EntitiesBackupHandler { +class EntitiesBackupHandler : public BackupHandlerInterface { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : _entitiesFilePath(entitiesFilePath), diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/BackupSupervisor.h index 9fedcca19b..0d0d21a174 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/BackupSupervisor.h @@ -24,6 +24,8 @@ #include +#include "BackupHandler.h" + class QuaZip; struct AssetServerBackup { @@ -32,7 +34,7 @@ struct AssetServerBackup { bool corruptedBackup; }; -class BackupSupervisor : public QObject { +class BackupSupervisor : public QObject, public BackupHandlerInterface { Q_OBJECT public: diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 66655ea966..2b990b170e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -39,7 +39,8 @@ static const QString DATETIME_FORMAT { "yyyy-MM-dd_HH-mm-ss" }; static const QString DATETIME_FORMAT_RE { "\\d{4}-\\d{2}-\\d{2}_\\d{2}-\\d{2}-\\d{2}" }; static const QString AUTOMATIC_BACKUP_PREFIX { "autobackup-" }; static const QString MANUAL_BACKUP_PREFIX { "backup-" }; -void DomainContentBackupManager::addBackupHandler(BackupHandler handler) { + +void DomainContentBackupManager::addBackupHandler(BackupHandlerPointer handler) { _backupHandlers.push_back(std::move(handler)); } @@ -238,7 +239,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, success = false; } else { for (auto& handler : _backupHandlers) { - handler.recoverBackup(zip); + handler->recoverBackup(zip); } qDebug() << "Successfully recovered from " << backupName; @@ -339,7 +340,7 @@ void DomainContentBackupManager::load() { } for (auto& handler : _backupHandlers) { - handler.loadBackup(zip); + handler->loadBackup(zip); } zip.close(); @@ -402,7 +403,7 @@ void DomainContentBackupManager::consolidate(QString fileName) { } for (auto& handler : _backupHandlers) { - handler.consolidateBackup(zip); + handler->consolidateBackup(zip); } zip.close(); @@ -437,7 +438,7 @@ std::pair DomainContentBackupManager::createBackup(const QString& } for (auto& handler : _backupHandlers) { - handler.createBackup(zip); + handler->createBackup(zip); } zip.close(); diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 5cf8d4698f..a3606929d5 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -50,7 +50,7 @@ public: int persistInterval = DEFAULT_PERSIST_INTERVAL, bool debugTimestampNow = false); - void addBackupHandler(BackupHandler handler); + void addBackupHandler(BackupHandlerPointer handler); std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist @@ -82,7 +82,7 @@ protected: private: const QString _backupDirectory; - std::vector _backupHandlers; + std::vector _backupHandlers; int _persistInterval { 0 }; int64_t _lastCheck { 0 }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index da8527bf16..a8ceebd6e7 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,8 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath())); - _contentManager->addBackupHandler(new BackupSupervisor(getContentBackupDir())); + _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new BackupSupervisor(getContentBackupDir()))); _contentManager->initialize(true); qDebug() << "Existing backups:"; From 4482f9c83c67def4fef5444a52f6d85e531ce53f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:49:12 -0800 Subject: [PATCH 028/157] Queue all requests until the AS is fully setup --- assignment-client/src/assets/AssetServer.cpp | 45 +++++++++++++++++--- assignment-client/src/assets/AssetServer.h | 5 +++ domain-server/src/BackupSupervisor.cpp | 5 +-- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 1ae65290ff..0be557bccd 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -257,12 +257,10 @@ AssetServer::AssetServer(ReceivedMessage& message) : _transferTaskPool.setMaxThreadCount(TASK_POOL_THREAD_COUNT); _bakingTaskPool.setMaxThreadCount(1); + // Queue all requests until the Asset Server is fully setup auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); - packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); - packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); - packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); - + packetReceiver.registerListenerForTypes({ PacketType::AssetGet, PacketType::AssetGetInfo, PacketType::AssetUpload, PacketType::AssetMappingOperation }, this, "queueRequests"); + #ifdef Q_OS_WIN updateConsumedCores(); QTimer* timer = new QTimer(this); @@ -417,6 +415,43 @@ void AssetServer::completeSetup() { PathUtils::removeTemporaryApplicationDirs(); PathUtils::removeTemporaryApplicationDirs("Oven"); + + // We're fully setup, remove the request queueing and replay all requests + auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.unregisterListener(this); + packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); + packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); + packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); + packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation"); + + replayRequests(); +} + +void AssetServer::queueRequests(QSharedPointer packet, SharedNodePointer senderNode) { + _queuedRequests.push_back({ packet, senderNode }); +} + +void AssetServer::replayRequests() { + for (const auto& request : _queuedRequests) { + switch (request.first->getType()) { + case PacketType::AssetGet: + handleAssetGet(request.first, request.second); + break; + case PacketType::AssetGetInfo: + handleAssetGetInfo(request.first, request.second); + break; + case PacketType::AssetUpload: + handleAssetUpload(request.first, request.second); + break; + case PacketType::AssetMappingOperation: + handleAssetMappingOperation(request.first, request.second); + break; + default: + qWarning() << "Unknown queued request type:" << request.first->getType(); + break; + } + } + _queuedRequests.clear(); } void AssetServer::cleanupUnmappedFiles() { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index c6336a3a4d..b8aac800ed 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -49,6 +49,7 @@ public slots: private slots: void completeSetup(); + void queueRequests(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetGetInfo(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetGet(QSharedPointer packet, SharedNodePointer senderNode); void handleAssetUpload(QSharedPointer packetList, SharedNodePointer senderNode); @@ -57,6 +58,8 @@ private slots: void sendStatsPacket() override; private: + void replayRequests(); + void handleGetMappingOperation(ReceivedMessage& message, NLPacketList& replyPacket); void handleGetAllMappingOperation(NLPacketList& replyPacket); void handleSetMappingOperation(ReceivedMessage& message, bool hasWriteAccess, NLPacketList& replyPacket); @@ -120,6 +123,8 @@ private: QHash> _pendingBakes; QThreadPool _bakingTaskPool; + QVector, SharedNodePointer>> _queuedRequests; + bool _wasColorTextureCompressionEnabled { false }; bool _wasGrayscaleTextureCompressionEnabled { false }; bool _wasNormalTextureCompressionEnabled { false }; diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/BackupSupervisor.cpp index 869f85c6cc..0cbded4e43 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/BackupSupervisor.cpp @@ -46,9 +46,8 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { - // Give the Asset Server some time to bootup. - static constexpr int ASSET_SERVER_BOOTUP_MARGIN = 1 * 1000; - _mappingsRefreshTimer.start(ASSET_SERVER_BOOTUP_MARGIN); + // run immediately for the first time. + _mappingsRefreshTimer.start(0); } }); } From d8d05fe0456831cd9dadde546c3635f09eafa46c Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 17:24:00 -0800 Subject: [PATCH 029/157] Rename backup supervisor --- ...Supervisor.cpp => AssetsBackupHandler.cpp} | 42 +++++++++---------- ...ckupSupervisor.h => AssetsBackupHandler.h} | 24 +++++------ domain-server/src/DomainServer.cpp | 4 +- domain-server/src/DomainServer.h | 2 +- 4 files changed, 36 insertions(+), 36 deletions(-) rename domain-server/src/{BackupSupervisor.cpp => AssetsBackupHandler.cpp} (93%) rename domain-server/src/{BackupSupervisor.h => AssetsBackupHandler.h} (85%) diff --git a/domain-server/src/BackupSupervisor.cpp b/domain-server/src/AssetsBackupHandler.cpp similarity index 93% rename from domain-server/src/BackupSupervisor.cpp rename to domain-server/src/AssetsBackupHandler.cpp index 0cbded4e43..3dc4851762 100644 --- a/domain-server/src/BackupSupervisor.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -1,5 +1,5 @@ // -// BackupSupervisor.cpp +// AssetsBackupHandler.cpp // domain-server/src // // Created by Clement Brisset on 1/12/18. @@ -9,7 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include #include @@ -31,7 +31,7 @@ using namespace std; Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); -BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : +AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _assetsDirectory(backupDirectory + ASSETS_DIR) { // Make sure the asset directory exists. @@ -41,7 +41,7 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); - QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &BackupSupervisor::refreshMappings); + QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); auto nodeList = DependencyManager::get(); QObject::connect(nodeList.data(), &LimitedNodeList::nodeAdded, this, [this](SharedNodePointer node) { @@ -53,7 +53,7 @@ BackupSupervisor::BackupSupervisor(const QString& backupDirectory) : } -void BackupSupervisor::refreshAssetsOnDisk() { +void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); @@ -64,7 +64,7 @@ void BackupSupervisor::refreshAssetsOnDisk() { } -void BackupSupervisor::refreshAssetsInBackups() { +void AssetsBackupHandler::refreshAssetsInBackups() { _assetsInBackups.clear(); for (const auto& backup : _backups) { for (const auto& mapping : backup.mappings) { @@ -73,7 +73,7 @@ void BackupSupervisor::refreshAssetsInBackups() { } } -void BackupSupervisor::checkForMissingAssets() { +void AssetsBackupHandler::checkForMissingAssets() { vector missingAssets; set_difference(begin(_assetsInBackups), end(_assetsInBackups), begin(_assetsOnDisk), end(_assetsOnDisk), @@ -83,7 +83,7 @@ void BackupSupervisor::checkForMissingAssets() { } } -void BackupSupervisor::checkForAssetsToDelete() { +void AssetsBackupHandler::checkForAssetsToDelete() { vector deprecatedAssets; set_difference(begin(_assetsOnDisk), end(_assetsOnDisk), begin(_assetsInBackups), end(_assetsInBackups), @@ -101,7 +101,7 @@ void BackupSupervisor::checkForAssetsToDelete() { } } -void BackupSupervisor::loadBackup(QuaZip& zip) { +void AssetsBackupHandler::loadBackup(QuaZip& zip) { _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); @@ -151,7 +151,7 @@ void BackupSupervisor::loadBackup(QuaZip& zip) { return; } -void BackupSupervisor::createBackup(QuaZip& zip) { +void AssetsBackupHandler::createBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already an operation in progress."; return; @@ -192,7 +192,7 @@ void BackupSupervisor::createBackup(QuaZip& zip) { _backups.push_back(backup); } -void BackupSupervisor::recoverBackup(QuaZip& zip) { +void AssetsBackupHandler::recoverBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; return; @@ -225,7 +225,7 @@ void BackupSupervisor::recoverBackup(QuaZip& zip) { restoreAllAssets(); } -void BackupSupervisor::deleteBackup(QuaZip& zip) { +void AssetsBackupHandler::deleteBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is a backup/restore in progress."; return; @@ -243,7 +243,7 @@ void BackupSupervisor::deleteBackup(QuaZip& zip) { checkForAssetsToDelete(); } -void BackupSupervisor::consolidateBackup(QuaZip& zip) { +void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { if (operationInProgress()) { qCWarning(backup_supervisor) << "There is a backup/restore in progress."; return; @@ -285,7 +285,7 @@ void BackupSupervisor::consolidateBackup(QuaZip& zip) { } -void BackupSupervisor::refreshMappings() { +void AssetsBackupHandler::refreshMappings() { auto assetClient = DependencyManager::get(); auto request = assetClient->createGetAllMappingsRequest(); @@ -314,7 +314,7 @@ void BackupSupervisor::refreshMappings() { request->start(); } -void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings) { +void AssetsBackupHandler::downloadMissingFiles(const AssetUtils::Mappings& mappings) { auto wasEmpty = _assetsLeftToRequest.empty(); for (const auto& mapping : mappings) { @@ -330,7 +330,7 @@ void BackupSupervisor::downloadMissingFiles(const AssetUtils::Mappings& mappings } } -void BackupSupervisor::downloadNextMissingFile() { +void AssetsBackupHandler::downloadNextMissingFile() { if (_assetsLeftToRequest.empty()) { return; } @@ -360,7 +360,7 @@ void BackupSupervisor::downloadNextMissingFile() { assetRequest->start(); } -bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { +bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data) { QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::WriteOnly)) { @@ -380,7 +380,7 @@ bool BackupSupervisor::writeAssetFile(const AssetUtils::AssetHash& hash, const Q return true; } -void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, +void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mappings& currentMappings, const AssetUtils::Mappings& newMappings) { _mappingsLeftToSet.reserve((int)newMappings.size()); _assetsLeftToUpload.reserve((int)newMappings.size()); @@ -415,11 +415,11 @@ void BackupSupervisor::computeServerStateDifference(const AssetUtils::Mappings& qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); } -void BackupSupervisor::restoreAllAssets() { +void AssetsBackupHandler::restoreAllAssets() { restoreNextAsset(); } -void BackupSupervisor::restoreNextAsset() { +void AssetsBackupHandler::restoreNextAsset() { if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -447,7 +447,7 @@ void BackupSupervisor::restoreNextAsset() { request->start(); } -void BackupSupervisor::updateMappings() { +void AssetsBackupHandler::updateMappings() { auto assetClient = DependencyManager::get(); for (const auto& mapping : _mappingsLeftToSet) { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); diff --git a/domain-server/src/BackupSupervisor.h b/domain-server/src/AssetsBackupHandler.h similarity index 85% rename from domain-server/src/BackupSupervisor.h rename to domain-server/src/AssetsBackupHandler.h index 0d0d21a174..184f30ab9b 100644 --- a/domain-server/src/BackupSupervisor.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -1,5 +1,5 @@ // -// BackupSupervisor.h +// AssetsBackupHandler.h // domain-server/src // // Created by Clement Brisset on 1/12/18. @@ -9,8 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef hifi_BackupSupervisor_h -#define hifi_BackupSupervisor_h +#ifndef hifi_AssetsBackupHandler_h +#define hifi_AssetsBackupHandler_h #include #include @@ -28,17 +28,11 @@ class QuaZip; -struct AssetServerBackup { - QString filePath; - AssetUtils::Mappings mappings; - bool corruptedBackup; -}; - -class BackupSupervisor : public QObject, public BackupHandlerInterface { +class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT public: - BackupSupervisor(const QString& backupDirectory); + AssetsBackupHandler(const QString& backupDirectory); void loadBackup(QuaZip& zip); void createBackup(QuaZip& zip); @@ -75,6 +69,12 @@ private: quint64 _lastMappingsRefresh { 0 }; AssetUtils::Mappings _currentMappings; + struct AssetServerBackup { + QString filePath; + AssetUtils::Mappings mappings; + bool corruptedBackup; + }; + bool _operationInProgress { false }; // Internal storage for backups on disk @@ -93,4 +93,4 @@ private: int _mappingRequestsInFlight { 0 }; }; -#endif /* hifi_BackupSupervisor_h */ +#endif /* hifi_AssetsBackupHandler_h */ diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index a8ceebd6e7..11b6a2d441 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -45,7 +45,7 @@ #include #include -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -297,7 +297,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new BackupSupervisor(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); _contentManager->initialize(true); qDebug() << "Existing backups:"; diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index ee0350665e..afe2a1cc7c 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -26,7 +26,7 @@ #include #include -#include "BackupSupervisor.h" +#include "AssetsBackupHandler.h" #include "DomainGatekeeper.h" #include "DomainMetadata.h" #include "DomainServerSettingsManager.h" From 9fca92facd27f12a0daff1455b0dc560fda4db46 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 18:11:55 -0800 Subject: [PATCH 030/157] Move EntitiesBackupHandler to its own file --- domain-server/src/AssetsBackupHandler.h | 3 - domain-server/src/BackupHandler.h | 79 +------------------ .../src/DomainContentBackupManager.cpp | 6 +- .../src/DomainContentBackupManager.h | 1 + domain-server/src/DomainServer.cpp | 1 + domain-server/src/EntitiesBackupHandler.cpp | 73 +++++++++++++++++ domain-server/src/EntitiesBackupHandler.h | 42 ++++++++++ 7 files changed, 124 insertions(+), 81 deletions(-) create mode 100644 domain-server/src/EntitiesBackupHandler.cpp create mode 100644 domain-server/src/EntitiesBackupHandler.h diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 184f30ab9b..b78206b7b1 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -21,13 +21,10 @@ #include #include - #include #include "BackupHandler.h" -class QuaZip; - class AssetsBackupHandler : public QObject, public BackupHandlerInterface { Q_OBJECT diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index eb9c35f236..8599dafb29 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -14,9 +14,7 @@ #include -#include - -#include +class QuaZip; class BackupHandlerInterface { public: @@ -28,80 +26,7 @@ public: virtual void deleteBackup(QuaZip& zip) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; + using BackupHandlerPointer = std::unique_ptr; -#include -#include - -class EntitiesBackupHandler : public BackupHandlerInterface { -public: - EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : - _entitiesFilePath(entitiesFilePath), - _entitiesReplacementFilePath(entitiesReplacementFilePath) {} - - void loadBackup(QuaZip& zip) {} - - // Create a skeleton backup - void createBackup(QuaZip& zip) { - QFile entitiesFile { _entitiesFilePath }; - - if (entitiesFile.open(QIODevice::ReadOnly)) { - QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); - zipFile.write(entitiesFile.readAll()); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); - } - } - } - - // Recover from a full backup - void recoverBackup(QuaZip& zip) { - if (!zip.setCurrentFile("models.json.gz")) { - qWarning() << "Failed to find models.json.gz while recovering backup"; - return; - } - QuaZipFile zipFile { &zip }; - if (!zipFile.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open models.json.gz in backup"; - return; - } - auto rawData = zipFile.readAll(); - - zipFile.close(); - - OctreeUtils::RawOctreeData data; - if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { - qCritical() << "Unable to parse octree data during backup recovery"; - return; - } - - data.resetIdAndVersion(); - - if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); - return; - } - - QFile entitiesFile { _entitiesReplacementFilePath }; - - if (entitiesFile.open(QIODevice::WriteOnly)) { - entitiesFile.write(data.toGzippedByteArray()); - } - } - - // Delete a skeleton backup - void deleteBackup(QuaZip& zip) { - } - - // Create a full backup - void consolidateBackup(QuaZip& zip) { - } - -private: - QString _entitiesFilePath; - QString _entitiesReplacementFilePath; -}; - #endif /* hifi_BackupHandler_h */ diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 2b990b170e..f39737c92e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -9,6 +9,8 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "DomainContentBackupManager.h" + #include #include @@ -25,13 +27,15 @@ #include #include +#include + #include #include #include #include #include "DomainServer.h" -#include "DomainContentBackupManager.h" + const int DomainContentBackupManager::DEFAULT_PERSIST_INTERVAL = 1000 * 30; // every 30 seconds // Backup format looks like: daily_backup-TIMESTAMP.zip diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index a3606929d5..1e1b2360a8 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -16,6 +16,7 @@ #include #include +#include #include diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 11b6a2d441..4c72423f74 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -47,6 +47,7 @@ #include "AssetsBackupHandler.h" #include "DomainServerNodeData.h" +#include "EntitiesBackupHandler.h" #include "NodeConnectionData.h" #include diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp new file mode 100644 index 0000000000..a95d68b007 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -0,0 +1,73 @@ +// +// EntitiesBackupHandler.cpp +// domain-server/src +// +// Created by Clement Brisset on 2/14/18. +// Copyright 2018 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 "EntitiesBackupHandler.h" + +#include + +#include +#include + +#include + +EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : + _entitiesFilePath(entitiesFilePath), + _entitiesReplacementFilePath(entitiesReplacementFilePath) +{ +} + +void EntitiesBackupHandler::createBackup(QuaZip& zip) { + QFile entitiesFile { _entitiesFilePath }; + + if (entitiesFile.open(QIODevice::ReadOnly)) { + QuaZipFile zipFile { &zip }; + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.write(entitiesFile.readAll()); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + } + } +} + +void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { + if (!zip.setCurrentFile("models.json.gz")) { + qWarning() << "Failed to find models.json.gz while recovering backup"; + return; + } + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open models.json.gz in backup"; + return; + } + auto rawData = zipFile.readAll(); + + zipFile.close(); + + OctreeUtils::RawOctreeData data; + if (!OctreeUtils::readOctreeDataInfoFromData(rawData, &data)) { + qCritical() << "Unable to parse octree data during backup recovery"; + return; + } + + data.resetIdAndVersion(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); + return; + } + + QFile entitiesFile { _entitiesReplacementFilePath }; + + if (entitiesFile.open(QIODevice::WriteOnly)) { + entitiesFile.write(data.toGzippedByteArray()); + } +} diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h new file mode 100644 index 0000000000..6f66483a87 --- /dev/null +++ b/domain-server/src/EntitiesBackupHandler.h @@ -0,0 +1,42 @@ +// +// EntitiesBackupHandler.h +// domain-server/src +// +// Created by Clement Brisset on 2/14/18. +// Copyright 2018 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 +// + +#ifndef hifi_EntitiesBackupHandler_h +#define hifi_EntitiesBackupHandler_h + +#include + +#include "BackupHandler.h" + +class EntitiesBackupHandler : public BackupHandlerInterface { +public: + EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath); + + void loadBackup(QuaZip& zip) {} + + // Create a skeleton backup + void createBackup(QuaZip& zip); + + // Recover from a full backup + void recoverBackup(QuaZip& zip); + + // Delete a skeleton backup + void deleteBackup(QuaZip& zip) {} + + // Create a full backup + void consolidateBackup(QuaZip& zip) {} + +private: + QString _entitiesFilePath; + QString _entitiesReplacementFilePath; +}; + +#endif /* hifi_EntitiesBackupHandler_h */ From d6e281408144f0db5bb102ca4a0b99dd460cd63f Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 11:25:07 -0800 Subject: [PATCH 031/157] Write assets to disk when recovering full backup --- domain-server/src/AssetsBackupHandler.cpp | 152 ++++++++++++++-------- domain-server/src/AssetsBackupHandler.h | 1 + 2 files changed, 99 insertions(+), 54 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index 3dc4851762..a9f56a0c5b 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -23,13 +24,14 @@ #include #include -const QString ASSETS_DIR = "/assets/"; -const QString MAPPINGS_FILE = "mappings.json"; +static const QString ASSETS_DIR = "/assets/"; +static const QString MAPPINGS_FILE = "mappings.json"; +static const QString ZIP_ASSETS_FOLDER = "files"; using namespace std; -Q_DECLARE_LOGGING_CATEGORY(backup_supervisor) -Q_LOGGING_CATEGORY(backup_supervisor, "hifi.backup-supervisor"); +Q_DECLARE_LOGGING_CATEGORY(asset_backup) +Q_LOGGING_CATEGORY(asset_backup, "hifi.asset-backup"); AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _assetsDirectory(backupDirectory + ASSETS_DIR) @@ -39,6 +41,10 @@ AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : refreshAssetsOnDisk(); + setupRefreshTimer(); +} + +void AssetsBackupHandler::setupRefreshTimer() { _mappingsRefreshTimer.setTimerType(Qt::CoarseTimer); _mappingsRefreshTimer.setSingleShot(true); QObject::connect(&_mappingsRefreshTimer, &QTimer::timeout, this, &AssetsBackupHandler::refreshMappings); @@ -50,9 +56,14 @@ AssetsBackupHandler::AssetsBackupHandler(const QString& backupDirectory) : _mappingsRefreshTimer.start(0); } }); + QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // run immediately for the first time. + _mappingsRefreshTimer.stop(); + } + }); } - void AssetsBackupHandler::refreshAssetsOnDisk() { QDir assetsDir { _assetsDirectory }; auto assetNames = assetsDir.entryList(QDir::Files); @@ -79,7 +90,7 @@ void AssetsBackupHandler::checkForMissingAssets() { begin(_assetsOnDisk), end(_assetsOnDisk), back_inserter(missingAssets)); if (missingAssets.size() > 0) { - qCWarning(backup_supervisor) << "Found" << missingAssets.size() << "assets missing."; + qCWarning(asset_backup) << "Found" << missingAssets.size() << "backup assets missing from disk."; } } @@ -90,24 +101,26 @@ void AssetsBackupHandler::checkForAssetsToDelete() { back_inserter(deprecatedAssets)); if (deprecatedAssets.size() > 0) { - qCDebug(backup_supervisor) << "Found" << deprecatedAssets.size() << "assets to delete."; + qCDebug(asset_backup) << "Found" << deprecatedAssets.size() << "backup assets to delete from disk."; if (_allBackupsLoadedSuccessfully) { for (const auto& hash : deprecatedAssets) { QFile::remove(_assetsDirectory + hash); } } else { - qCWarning(backup_supervisor) << "Some backups did not load properly, aborting deleting for safety."; + qCWarning(asset_backup) << "Some backups did not load properly, aborting delete operation for safety."; } } } void AssetsBackupHandler::loadBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + _backups.push_back({ zip.getZipName(), {}, false }); auto& backup = _backups.back(); if (!zip.setCurrentFile(MAPPINGS_FILE)) { - qCCritical(backup_supervisor) << "Failed to find" << MAPPINGS_FILE << "while recovering backup"; - qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + qCCritical(asset_backup) << "Failed to find" << MAPPINGS_FILE << "while loading backup"; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -115,8 +128,8 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QFile::ReadOnly)) { - qCCritical(backup_supervisor) << "Could not open backup file:" << zip.getZipName(); - qCCritical(backup_supervisor) << " Error:" << zip.getZipError(); + qCCritical(asset_backup) << "Could not unzip backup file for load:" << zip.getZipName(); + qCCritical(asset_backup) << " Error:" << zip.getZipError(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -125,8 +138,8 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { QJsonParseError error; auto document = QJsonDocument::fromJson(zipFile.readAll(), &error); if (document.isNull() || !document.isObject()) { - qCCritical(backup_supervisor) << "Could not parse backup file to JSON object:" << zip.getZipName(); - qCCritical(backup_supervisor) << " Error:" << error.errorString(); + qCCritical(asset_backup) << "Could not parse backup file to JSON object for load:" << zip.getZipName(); + qCCritical(asset_backup) << " Error:" << error.errorString(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; return; @@ -138,33 +151,37 @@ void AssetsBackupHandler::loadBackup(QuaZip& zip) { const auto& assetHash = it.value().toString(); if (!AssetUtils::isValidHash(assetHash)) { - qCCritical(backup_supervisor) << "Corrupted mapping in backup file" << zip.getZipName() << ":" << it.key(); + qCCritical(asset_backup) << "Corrupted mapping in loading backup file" << zip.getZipName() << ":" << it.key(); backup.corruptedBackup = true; _allBackupsLoadedSuccessfully = false; - return; + continue; } backup.mappings[assetPath] = assetHash; _assetsInBackups.insert(assetHash); } + checkForMissingAssets(); + checkForAssetsToDelete(); return; } void AssetsBackupHandler::createBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is already an operation in progress."; + qCWarning(asset_backup) << "There is already an operation in progress."; return; } if (_lastMappingsRefresh == 0) { - qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Backing up asset mappings that might be stale."; + qCWarning(asset_backup) << "Backing up asset mappings that might be stale."; } AssetServerBackup backup; @@ -180,43 +197,65 @@ void AssetsBackupHandler::createBackup(QuaZip& zip) { QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(MAPPINGS_FILE))) { - qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); return; } zipFile.write(document.toJson()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); return; } _backups.push_back(backup); } void AssetsBackupHandler::recoverBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is already a backup/restore in progress."; + qCWarning(asset_backup) << "There is already a backup/restore in progress."; return; } if (_lastMappingsRefresh == 0) { - qCWarning(backup_supervisor) << "Current mappings not yet loaded."; + qCWarning(asset_backup) << "Current mappings not yet loaded."; return; } static constexpr quint64 MAX_REFRESH_TIME = 15 * 60 * 1000 * 1000; if (usecTimestampNow() - _lastMappingsRefresh > MAX_REFRESH_TIME) { - qCWarning(backup_supervisor) << "Current asset mappings that might be stale."; + qCWarning(asset_backup) << "Current asset mappings that might be stale."; } - startOperation(); - auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to restore."; - stopOperation(); - return; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to restore."; + + loadBackup(zip); + + QuaZipDir zipDir { &zip, ZIP_ASSETS_FOLDER }; + + auto assetNames = zipDir.entryList(QDir::Files); + for (const auto& asset : assetNames) { + if (AssetUtils::isValidHash(asset)) { + if (!zip.setCurrentFile(MAPPINGS_FILE)) { + qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + continue; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QFile::ReadOnly)) { + qCCritical(asset_backup) << "Could not unzip asset file:" << asset; + qCCritical(asset_backup) << " Error:" << zip.getZipError(); + continue; + } + + writeAssetFile(asset, zipFile.readAll()); + } + } } const auto& newMappings = it->mappings; @@ -226,8 +265,10 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { } void AssetsBackupHandler::deleteBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } @@ -235,7 +276,7 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { return value.filePath == zip.getZipName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to delete."; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to delete."; return; } @@ -244,8 +285,10 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { } void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { + Q_ASSERT(QThread::currentThread() == thread()); + if (operationInProgress()) { - qCWarning(backup_supervisor) << "There is a backup/restore in progress."; + qCWarning(asset_backup) << "There is a backup/restore in progress."; return; } QFileInfo zipInfo(zip.getZipName()); @@ -255,7 +298,7 @@ void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { return info.fileName() == zipInfo.fileName(); }); if (it == end(_backups)) { - qCDebug(backup_supervisor) << "Could not find backup" << zip.getZipName() << "to consolidate."; + qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to consolidate."; return; } @@ -265,20 +308,19 @@ void AssetsBackupHandler::consolidateBackup(QuaZip& zip) { QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::ReadOnly)) { - qCCritical(backup_supervisor) << "Could not open asset file" << file.fileName(); + qCCritical(asset_backup) << "Could not open asset file" << file.fileName(); continue; } QuaZipFile zipFile { &zip }; - static const QString ZIP_ASSETS_FOLDER = "files/"; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + hash))) { - qCDebug(backup_supervisor) << "Could not open zip file:" << zipFile.getZipError(); + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ZIP_ASSETS_FOLDER + "/" + hash))) { + qCDebug(asset_backup) << "Could not open zip file:" << zipFile.getZipError(); continue; } zipFile.write(file.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCDebug(backup_supervisor) << "Could not close zip file: " << zipFile.getZipError(); + qCDebug(asset_backup) << "Could not close zip file: " << zipFile.getZipError(); continue; } } @@ -300,8 +342,8 @@ void AssetsBackupHandler::refreshMappings() { downloadMissingFiles(_currentMappings); } else { - qCCritical(backup_supervisor) << "Could not refresh asset server mappings."; - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Could not refresh asset server mappings."; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } request->deleteLater(); @@ -341,14 +383,14 @@ void AssetsBackupHandler::downloadNextMissingFile() { QObject::connect(assetRequest, &AssetRequest::finished, this, [this](AssetRequest* request) { if (request->getError() == AssetRequest::NoError) { - qCDebug(backup_supervisor) << "Backing up asset" << request->getHash(); + qCDebug(asset_backup) << "Backing up asset" << request->getHash(); bool success = writeAssetFile(request->getHash(), request->getData()); if (!success) { - qCCritical(backup_supervisor) << "Failed to write asset file" << request->getHash(); + qCCritical(asset_backup) << "Failed to write asset file" << request->getHash(); } } else { - qCCritical(backup_supervisor) << "Failed to backup asset" << request->getHash(); + qCCritical(asset_backup) << "Failed to backup asset" << request->getHash(); } _assetsLeftToRequest.erase(request->getHash()); @@ -364,13 +406,13 @@ bool AssetsBackupHandler::writeAssetFile(const AssetUtils::AssetHash& hash, cons QDir assetsDir { _assetsDirectory }; QFile file { assetsDir.filePath(hash) }; if (!file.open(QFile::WriteOnly)) { - qCCritical(backup_supervisor) << "Could not open backup file" << file.fileName(); + qCCritical(asset_backup) << "Could not open asset file for write:" << file.fileName(); return false; } auto bytesWritten = file.write(data); if (bytesWritten != data.size()) { - qCCritical(backup_supervisor) << "Could not write data to file" << file.fileName(); + qCCritical(asset_backup) << "Could not write data to file" << file.fileName(); file.remove(); return false; } @@ -410,9 +452,9 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } - qCDebug(backup_supervisor) << "Mappings to set:" << _mappingsLeftToSet.size(); - qCDebug(backup_supervisor) << "Mappings to del:" << _mappingsLeftToDelete.size(); - qCDebug(backup_supervisor) << "Assets to upload:" << _assetsLeftToUpload.size(); + qCDebug(asset_backup) << "Mappings to set:" << _mappingsLeftToSet.size(); + qCDebug(asset_backup) << "Mappings to del:" << _mappingsLeftToDelete.size(); + qCDebug(asset_backup) << "Assets to upload:" << _assetsLeftToUpload.size(); } void AssetsBackupHandler::restoreAllAssets() { @@ -420,6 +462,8 @@ void AssetsBackupHandler::restoreAllAssets() { } void AssetsBackupHandler::restoreNextAsset() { + startOperation(); + if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -435,8 +479,8 @@ void AssetsBackupHandler::restoreNextAsset() { QObject::connect(request, &AssetUpload::finished, this, [this](AssetUpload* request) { if (request->getError() != AssetUpload::NoError) { - qCCritical(backup_supervisor) << "Failed to restore asset:" << request->getFilename(); - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to restore asset:" << request->getFilename(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } restoreNextAsset(); @@ -453,8 +497,8 @@ void AssetsBackupHandler::updateMappings() { auto request = assetClient->createSetMappingRequest(mapping.first, mapping.second); QObject::connect(request, &SetMappingRequest::finished, this, [this](SetMappingRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCCritical(backup_supervisor) << "Failed to set mapping:" << request->getPath(); - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to set mapping:" << request->getPath(); + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { @@ -472,8 +516,8 @@ void AssetsBackupHandler::updateMappings() { auto request = assetClient->createDeleteMappingsRequest(_mappingsLeftToDelete); QObject::connect(request, &DeleteMappingsRequest::finished, this, [this](DeleteMappingsRequest* request) { if (request->getError() != MappingRequest::NoError) { - qCCritical(backup_supervisor) << "Failed to delete mappings"; - qCCritical(backup_supervisor) << " Error:" << request->getErrorString(); + qCCritical(asset_backup) << "Failed to delete mappings"; + qCCritical(asset_backup) << " Error:" << request->getErrorString(); } if (--_mappingRequestsInFlight == 0) { diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index b78206b7b1..2ef454998e 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -40,6 +40,7 @@ public: bool operationInProgress() const { return _operationInProgress; } private: + void setupRefreshTimer(); void refreshMappings(); void refreshAssetsInBackups(); From 3297e39c144581788015b5ea4cd5bc52505fda0a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 13:39:18 -0800 Subject: [PATCH 032/157] CR --- domain-server/src/AssetsBackupHandler.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index a9f56a0c5b..ae9cb58343 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -58,7 +58,6 @@ void AssetsBackupHandler::setupRefreshTimer() { }); QObject::connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, [this](SharedNodePointer node) { if (node->getType() == NodeType::AssetServer) { - // run immediately for the first time. _mappingsRefreshTimer.stop(); } }); @@ -240,7 +239,7 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { auto assetNames = zipDir.entryList(QDir::Files); for (const auto& asset : assetNames) { if (AssetUtils::isValidHash(asset)) { - if (!zip.setCurrentFile(MAPPINGS_FILE)) { + if (!zip.setCurrentFile(zipDir.filePath(asset))) { qCCritical(asset_backup) << "Failed to find" << asset << "while recovering backup"; qCCritical(asset_backup) << " Error:" << zip.getZipError(); continue; From f624e1b464bd17755058f516e4ae114cc759041f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:10:51 -0800 Subject: [PATCH 033/157] add a content settings backup handler --- .../src/ContentSettingsBackupHandler.cpp | 66 +++++++++++++++++++ .../src/ContentSettingsBackupHandler.h | 35 ++++++++++ domain-server/src/DomainServer.cpp | 2 + .../src/DomainServerSettingsManager.h | 10 +-- domain-server/src/EntitiesBackupHandler.cpp | 14 ++-- 5 files changed, 116 insertions(+), 11 deletions(-) create mode 100644 domain-server/src/ContentSettingsBackupHandler.cpp create mode 100644 domain-server/src/ContentSettingsBackupHandler.h diff --git a/domain-server/src/ContentSettingsBackupHandler.cpp b/domain-server/src/ContentSettingsBackupHandler.cpp new file mode 100644 index 0000000000..be470bdd9f --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.cpp @@ -0,0 +1,66 @@ +// +// ContentSettingsBackupHandler.cpp +// domain-server/src +// +// Created by Stephen Birarda on 2/15/18. +// Copyright 2018 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 "ContentSettingsBackupHandler.h" + +#include +#include + +ContentSettingsBackupHandler::ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager) : + _settingsManager(domainServerSettingsManager) +{ + +} + +static const QString CONTENT_SETTINGS_BACKUP_FILENAME = "content-settings.json"; + +void ContentSettingsBackupHandler::createBackup(QuaZip& zip) { + + // grab the content settings as JSON,excluding default values and values hidden from backup + QJsonObject contentSettingsJSON = _settingsManager.settingsResponseObjectForType("", true, false, true, false, true); + + // make a QJSonDocument using the object + QJsonDocument contentSettingsDocument { contentSettingsJSON }; + + QuaZipFile zipFile { &zip }; + + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(CONTENT_SETTINGS_BACKUP_FILENAME)); + zipFile.write(contentSettingsDocument.toJson()); + zipFile.close(); + + if (zipFile.getZipError() != UNZ_OK) { + qCritical().nospace() << "Failed to zip " << CONTENT_SETTINGS_BACKUP_FILENAME << ": " << zipFile.getZipError(); + } +} + +void ContentSettingsBackupHandler::recoverBackup(QuaZip& zip) { + if (!zip.setCurrentFile(CONTENT_SETTINGS_BACKUP_FILENAME)) { + qWarning() << "Failed to find" << CONTENT_SETTINGS_BACKUP_FILENAME << "while recovering backup"; + return; + } + + QuaZipFile zipFile { &zip }; + if (!zipFile.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open" << CONTENT_SETTINGS_BACKUP_FILENAME << "in backup"; + return; + } + + auto rawData = zipFile.readAll(); + zipFile.close(); + + QJsonDocument jsonDocument = QJsonDocument::fromJson(rawData); + + if (!_settingsManager.restoreSettingsFromObject(jsonDocument.object(), ContentSettings)) { + qCritical() << "Failed to restore settings from" << CONTENT_SETTINGS_BACKUP_FILENAME << "in content archive"; + return; + } + +} diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h new file mode 100644 index 0000000000..932b7c0c3f --- /dev/null +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -0,0 +1,35 @@ +// +// ContentSettingsBackupHandler.h +// domain-server/src +// +// Created by Stephen Birarda on 2/15/18. +// Copyright 2018 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 +// + +#ifndef hifi_ContentSettingsBackupHandler_h +#define hifi_ContentSettingsBackupHandler_h + +#include "BackupHandler.h" +#include "DomainServerSettingsManager.h" + +class ContentSettingsBackupHandler : public BackupHandlerInterface { +public: + ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager); + + void loadBackup(QuaZip& zip) {}; + + void createBackup(QuaZip& zip); + + void recoverBackup(QuaZip& zip); + + void deleteBackup(QuaZip& zip) {}; + + void consolidateBackup(QuaZip& zip) {}; +private: + DomainServerSettingsManager& _settingsManager; +}; + +#endif // hifi_ContentSettingsBackupHandler_h diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 4c72423f74..d3bc5fdff1 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -46,6 +46,7 @@ #include #include "AssetsBackupHandler.h" +#include "ContentSettingsBackupHandler.h" #include "DomainServerNodeData.h" #include "EntitiesBackupHandler.h" #include "NodeConnectionData.h" @@ -299,6 +300,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); _contentManager->initialize(true); qDebug() << "Existing backups:"; diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 9b2427b344..4316534685 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -111,6 +111,11 @@ public: void debugDumpGroupsState(); + QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, + bool includeDomainSettings = true, bool includeContentSettings = true, + bool includeDefaults = true, bool isForBackup = false); + bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + signals: void updateNodePermissions(); void settingsUpdated(); @@ -130,9 +135,6 @@ private: QStringList _argumentList; QJsonArray filteredDescriptionArray(bool isContentSettings); - QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, - bool includeDomainSettings = true, bool includeContentSettings = true, - bool includeDefaults = true, bool isForBackup = false); bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, @@ -143,8 +145,6 @@ private: void splitSettingsDescription(); - bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); - double _descriptionVersion; QJsonArray _descriptionArray; diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index a95d68b007..6ad00d01c8 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -24,28 +24,30 @@ EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString e { } +static const QString ENTITIES_BACKUP_FILENAME = "models.json.gz"; + void EntitiesBackupHandler::createBackup(QuaZip& zip) { QFile entitiesFile { _entitiesFilePath }; if (entitiesFile.open(QIODevice::ReadOnly)) { QuaZipFile zipFile { &zip }; - zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo("models.json.gz", _entitiesFilePath)); + zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(ENTITIES_BACKUP_FILENAME, _entitiesFilePath)); zipFile.write(entitiesFile.readAll()); zipFile.close(); if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to zip models.json.gz: " << zipFile.getZipError(); + qCritical().nospace() << "Failed to zip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); } } } void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { - if (!zip.setCurrentFile("models.json.gz")) { - qWarning() << "Failed to find models.json.gz while recovering backup"; + if (!zip.setCurrentFile(ENTITIES_BACKUP_FILENAME)) { + qWarning() << "Failed to find" << ENTITIES_BACKUP_FILENAME << "while recovering backup"; return; } QuaZipFile zipFile { &zip }; if (!zipFile.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open models.json.gz in backup"; + qCritical() << "Failed to open" << ENTITIES_BACKUP_FILENAME << "in backup"; return; } auto rawData = zipFile.readAll(); @@ -61,7 +63,7 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { data.resetIdAndVersion(); if (zipFile.getZipError() != UNZ_OK) { - qCritical() << "Failed to unzip models.json.gz: " << zipFile.getZipError(); + qCritical().nospace() << "Failed to unzip " << ENTITIES_BACKUP_FILENAME << ": " << zipFile.getZipError(); return; } From f5cad5683dbb4028c76beafbdafddb249655d0b7 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:39:25 -0800 Subject: [PATCH 034/157] make sure backup handlers end up on the correct thread --- domain-server/src/DomainContentBackupManager.cpp | 3 --- domain-server/src/DomainServer.cpp | 12 ++++++++---- libraries/shared/src/GenericThread.cpp | 2 ++ libraries/shared/src/GenericThread.h | 1 + 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f39737c92e..c68ff0c6ea 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -155,9 +155,6 @@ bool DomainContentBackupManager::process() { } void DomainContentBackupManager::aboutToFinish() { - qCDebug(domain_server) << "Persist thread about to finish..."; - backup(); - qCDebug(domain_server) << "Persist thread done with about to finish..."; _stopThread = true; } diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index d3bc5fdff1..599f09ae94 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -298,9 +298,13 @@ DomainServer::DomainServer(int argc, char* argv[]) : maybeHandleReplacementEntityFile(); _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); - _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); - _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); + + connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ + _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new AssetsBackupHandler(getContentBackupDir()))); + _contentManager->addBackupHandler(BackupHandlerPointer(new ContentSettingsBackupHandler(_settingsManager))); + }); + _contentManager->initialize(true); qDebug() << "Existing backups:"; @@ -382,7 +386,7 @@ DomainServer::~DomainServer() { if (_contentManager) { _contentManager->aboutToFinish(); - _contentManager->terminating(); + _contentManager->terminate(); } } diff --git a/libraries/shared/src/GenericThread.cpp b/libraries/shared/src/GenericThread.cpp index 2e126f12c9..50655820af 100644 --- a/libraries/shared/src/GenericThread.cpp +++ b/libraries/shared/src/GenericThread.cpp @@ -38,6 +38,8 @@ void GenericThread::initialize(bool isThreaded, QThread::Priority priority) { _thread->setObjectName(objectName()); // when the worker thread is started, call our engine's run.. + + connect(_thread, &QThread::started, this, &GenericThread::started); connect(_thread, &QThread::started, this, &GenericThread::threadRoutine); connect(_thread, &QThread::finished, this, &GenericThread::finished); diff --git a/libraries/shared/src/GenericThread.h b/libraries/shared/src/GenericThread.h index 09872b32cd..c1f946d6aa 100644 --- a/libraries/shared/src/GenericThread.h +++ b/libraries/shared/src/GenericThread.h @@ -47,6 +47,7 @@ public slots: void threadRoutine(); signals: + void started(); void finished(); protected: From e06c95f5863d3aeb67f1c18d624acc25743e605f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 15:51:49 -0800 Subject: [PATCH 035/157] make settings manager methods used for backup/restore thread safe --- .../src/DomainServerSettingsManager.cpp | 24 +++++++++++++++++++ .../src/DomainServerSettingsManager.h | 6 +++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 8febbd5769..5e3c8e9cee 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -1196,6 +1197,16 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { + + if (thread() != QThread::currentThread()) { + bool success; + QMetaObject::invokeMethod(this, "restoreSettingsFromObject", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(bool, success), + Q_ARG(QJsonObject, settingsToRestore), + Q_ARG(SettingsType, settingsType)); + return success; + } + QJsonArray& filteredDescriptionArray = settingsType == DomainSettings ? _domainSettingsDescription : _contentSettingsDescription; @@ -1321,6 +1332,19 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includeDefaults, bool isForBackup) { QJsonObject responseObject; + if (thread() != QThread::currentThread()) { + QMetaObject::invokeMethod(this, "settingsResponseObjectForType", Qt::BlockingQueuedConnection, + Q_RETURN_ARG(QJsonObject, responseObject), + Q_ARG(const QString&, typeValue), + Q_ARG(bool, isAuthenticated), + Q_ARG(bool, includeDomainSettings), + Q_ARG(bool, includeContentSettings), + Q_ARG(bool, includeDefaults), + Q_ARG(bool, isForBackup)); + + return responseObject; + } + if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 4316534685..abc70751a8 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -111,10 +111,12 @@ public: void debugDumpGroupsState(); - QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, + /// thread safe method to retrieve a JSON representation of settings + Q_INVOKABLE QJsonObject settingsResponseObjectForType(const QString& typeValue, bool isAuthenticated = false, bool includeDomainSettings = true, bool includeContentSettings = true, bool includeDefaults = true, bool isForBackup = false); - bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + /// thread safe method to restore settings from a JSON object + Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); signals: void updateNodePermissions(); From e71f2fa38788c8f74f5ede92749eab7f296a5743 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 16:03:03 -0800 Subject: [PATCH 036/157] use QtHelpers macro for blocking invoke --- .../src/DomainServerSettingsManager.cpp | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 5e3c8e9cee..5f71890898 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -33,6 +33,8 @@ #include #include //for KillAvatarReason #include +#include + #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -1200,10 +1202,11 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings if (thread() != QThread::currentThread()) { bool success; - QMetaObject::invokeMethod(this, "restoreSettingsFromObject", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(bool, success), - Q_ARG(QJsonObject, settingsToRestore), - Q_ARG(SettingsType, settingsType)); + + BLOCKING_INVOKE_METHOD(this, "restoreSettingsFromObject", + Q_RETURN_ARG(bool, success), + Q_ARG(QJsonObject, settingsToRestore), + Q_ARG(SettingsType, settingsType)); return success; } @@ -1333,14 +1336,15 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt QJsonObject responseObject; if (thread() != QThread::currentThread()) { - QMetaObject::invokeMethod(this, "settingsResponseObjectForType", Qt::BlockingQueuedConnection, - Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(const QString&, typeValue), - Q_ARG(bool, isAuthenticated), - Q_ARG(bool, includeDomainSettings), - Q_ARG(bool, includeContentSettings), - Q_ARG(bool, includeDefaults), - Q_ARG(bool, isForBackup)); + + BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", + Q_RETURN_ARG(QJsonObject, responseObject), + Q_ARG(const QString&, typeValue), + Q_ARG(bool, isAuthenticated), + Q_ARG(bool, includeDomainSettings), + Q_ARG(bool, includeContentSettings), + Q_ARG(bool, includeDefaults), + Q_ARG(bool, isForBackup)); return responseObject; } From 9e99c5c744ea7621b37e4c0b7e36d84906ea82f4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 11:49:05 -0800 Subject: [PATCH 037/157] Add restart of ES during backup recovery --- domain-server/src/DomainServer.cpp | 2 ++ domain-server/src/EntitiesBackupHandler.cpp | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 599f09ae94..2f8d8f6d03 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1785,6 +1785,8 @@ QString DomainServer::getEntitiesReplacementFilePath() { void DomainServer::processOctreeDataRequestMessage(QSharedPointer message) { qDebug() << "Got request for octree data from " << message->getSenderSockAddr(); + maybeHandleReplacementEntityFile(); + bool remoteHasExistingData { false }; QUuid id; int version; diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index 6ad00d01c8..deb92ee0f6 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -16,6 +16,7 @@ #include #include +#include #include EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -71,5 +72,13 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data.toGzippedByteArray()); + entitiesFile.close(); + + auto nodeList = DependencyManager::get(); + nodeList->eachMatchingNode([](const SharedNodePointer& otherNode) -> bool { + return otherNode->getType() == NodeType::EntityServer; + }, [nodeList](const SharedNodePointer& otherNode) { + QMetaObject::invokeMethod(nodeList.data(), "killNodeWithUUID", Q_ARG(const QUuid&, otherNode->getUUID())); + }); } } From dd0b8a0c2fd3a22a82857c06e6027c56735222be Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 14:17:32 -0800 Subject: [PATCH 038/157] Add backup download API to DS --- .../src/DomainContentBackupManager.cpp | 78 +++++++++++++------ .../src/DomainContentBackupManager.h | 2 +- domain-server/src/DomainServer.cpp | 22 ++++++ 3 files changed, 76 insertions(+), 26 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index c68ff0c6ea..f56c41dacd 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -383,32 +383,60 @@ void DomainContentBackupManager::backup() { } } -void DomainContentBackupManager::consolidate(QString fileName) { - QDir backupDir { _backupDirectory }; - if (backupDir.exists()) { - auto filePath = backupDir.absoluteFilePath(fileName); - - auto copyFilePath = QDir::tempPath() + "/" + fileName; - - auto copySuccess = QFile::copy(filePath, copyFilePath); - if (!copySuccess) { - qCritical() << "Failed to create full backup."; - return; - } - - QuaZip zip(copyFilePath); - if (!zip.open(QuaZip::mdAdd)) { - qCritical() << "Could not open backup archive:" << filePath; - qCritical() << " ERROR:" << zip.getZipError(); - return; - } - - for (auto& handler : _backupHandlers) { - handler->consolidateBackup(zip); - } - - zip.close(); +void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, QString fileName) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "consolidateBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(const QString&, fileName)); + return; } + + QDir backupDir { _backupDirectory }; + if (!backupDir.exists()) { + qCritical() << "Backup directory does not exist, bailing consolidation of backup"; + promise->resolve({ { "success", false } }); + return; + } + + auto filePath = backupDir.absoluteFilePath(fileName); + + auto copyFilePath = QDir::tempPath() + "/" + fileName; + + { + QFile copyFile(copyFilePath); + copyFile.remove(); + copyFile.close(); + } + auto copySuccess = QFile::copy(filePath, copyFilePath); + if (!copySuccess) { + qCritical() << "Failed to create copy of backup."; + promise->resolve({ { "success", false } }); + return; + } + + QuaZip zip(copyFilePath); + if (!zip.open(QuaZip::mdAdd)) { + qCritical() << "Could not open backup archive:" << filePath; + qCritical() << " ERROR:" << zip.getZipError(); + promise->resolve({ { "success", false } }); + return; + } + + for (auto& handler : _backupHandlers) { + handler->consolidateBackup(zip); + } + + zip.close(); + + if (zip.getZipError() != UNZ_OK) { + qCritical() << "Failed to consolidate backup: " << zip.getZipError(); + promise->resolve({ { "success", false } }); + return; + } + + promise->resolve({ + { "success", true }, + { "backupFilePath", copyFilePath } + }); } void DomainContentBackupManager::createManualBackup(MiniPromise::Promise promise, const QString& name) { diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 1e1b2360a8..9ec7eb9950 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -62,6 +62,7 @@ public slots: void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); + void consolidateBackup(MiniPromise::Promise promise, QString fileName); signals: void loadCompleted(); @@ -73,7 +74,6 @@ protected: void load(); void backup(); - void consolidate(QString fileName); void removeOldBackupVersions(const BackupRule& rule); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); int64_t getMostRecentBackupTimeInSecs(const QString& format); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 2f8d8f6d03..b91e12a9cf 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2149,6 +2149,28 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QJsonDocument docJSON(rootJSON); connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + return true; + } else if (url.path().startsWith(URI_API_BACKUPS_ID)) { + auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); + auto deferred = makePromise("consolidateBackup"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + if (success) { + auto path = result["backupFilePath"].toString(); + auto file { std::unique_ptr(new QFile(path)) }; + if (file->open(QIODevice::ReadOnly)) { + connection->respond(HTTPConnection::StatusCode200, std::move(file)); + } else { + qCritical(domain_server) << "Unable to load consolidated backup at:" << path << result; + connection->respond(HTTPConnection::StatusCode500, "Error opening backup"); + } + } else { + connection->respond(HTTPConnection::StatusCode400); + } + }); + _contentManager->consolidateBackup(deferred, id); + return true; } else if (url.path() == URI_RESTART) { connection->respond(HTTPConnection::StatusCode200); From 2942a53a1d51b488d7e8818e180c25abb335f0a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 15:55:14 -0800 Subject: [PATCH 039/157] Add recovery mode and full backup information to DS --- .../src/DomainContentBackupManager.cpp | 91 +++++++++++++++++-- .../src/DomainContentBackupManager.h | 14 +-- domain-server/src/DomainServer.cpp | 28 ++---- 3 files changed, 96 insertions(+), 37 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f56c41dacd..54ea7e23d5 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -147,7 +147,24 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; - backup(); + if (_isRecovering) { + bool anyHandlerIsRecovering { false }; + for (auto& handler : _backupHandlers) { + bool handlerIsRecovering { false }; + float progress { 0.0f }; + //std::tie = handler->getRecoveryStatus(); + if (handlerIsRecovering) { + anyHandlerIsRecovering = true; + emit recoveryCompleted(); + break; + } + } + _isRecovering = anyHandlerIsRecovering; + } + + if (!_isRecovering) { + backup(); + } } } @@ -213,6 +230,13 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons return; } + if (_isRecovering && backupName == _recoveryFilename) { + promise->resolve({ + { "success", false } + }); + return; + } + QDir backupDir { _backupDirectory }; QFile backupFile { backupDir.filePath(backupName) }; auto success = backupFile.remove(); @@ -222,6 +246,13 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons } void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { + if (_isRecovering) { + promise->resolve({ + { "success", false } + }); + return; + }; + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "recoverFromBackup", Q_ARG(MiniPromise::Promise, promise), Q_ARG(const QString&, backupName)); @@ -239,11 +270,12 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, qWarning() << "Failed to unzip file: " << backupName; success = false; } else { + _isRecovering = true; for (auto& handler : _backupHandlers) { handler->recoverBackup(zip); } - qDebug() << "Successfully recovered from " << backupName; + qDebug() << "Successfully started recovering from " << backupName; success = true; } backupFile.close(); @@ -257,8 +289,11 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } -std::vector DomainContentBackupManager::getAllBackups() { - std::vector backups; +void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise promise) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + return; + } QDir backupDir { _backupDirectory }; auto matchingFiles = @@ -269,6 +304,8 @@ std::vector DomainContentBackupManager::getAllBackups() { QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + QVariantList backups; + for (const auto& fileInfo : matchingFiles) { auto fileName = fileInfo.fileName(); if (backupNameFormat.exactMatch(fileName)) { @@ -280,12 +317,50 @@ std::vector DomainContentBackupManager::getAllBackups() { continue; } - BackupItemInfo backup { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }; - backups.push_back(backup); + bool isAvailable { true }; + float availabilityProgress { 0.0f }; + for (auto& handler : _backupHandlers) { + bool handlerIsAvailable { false }; + float progress { 0.0f }; + //std::tie = handler->isAvailable(); + //isAvailable = isAvailable && !handlerIsAvailable); + //availabilityProgress += progress / _backupHandlers.size(); + } + + backups.push_back(QVariantMap({ + { "id", fileInfo.fileName() }, + { "name", name }, + { "createdAtMillis", createdAt.toMSecsSinceEpoch() }, + { "isAvailable", isAvailable }, + { "availabilityProgress", availabilityProgress }, + { "isManualBackup", type == MANUAL_BACKUP_PREFIX } + })); } } - return backups; + float recoveryProgress = 0.0f; + bool isRecovering = _isRecovering.load(); + if (_isRecovering) { + for (auto& handler : _backupHandlers) { + bool handlerIsRecovering { false }; + float progress { 0.0f }; + //std::tie = handler->getRecoveryStatus(); + recoveryProgress += progress / _backupHandlers.size(); + } + } + + QVariantMap status { + { "isRecovering", isRecovering }, + { "recoveringBackupId", _recoveryFilename }, + { "recoveryProgress", recoveryProgress } + }; + + QVariantMap info { + { "backups", backups }, + { "status", status } + }; + + promise->resolve(info); } void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) { @@ -433,6 +508,8 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, return; } + qDebug() << "copyFilePath" << copyFilePath; + promise->resolve({ { "success", true }, { "backupFilePath", copyFilePath } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 9ec7eb9950..790dff0fb4 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -24,14 +24,6 @@ #include -struct BackupItemInfo { - QString id; - QString name; - QString absolutePath; - QDateTime createdAt; - bool isManualBackup; -}; - class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -52,13 +44,13 @@ public: bool debugTimestampNow = false); void addBackupHandler(BackupHandlerPointer handler); - std::vector getAllBackups(); void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist void replaceData(QByteArray data); public slots: + void getAllBackupInformation(MiniPromise::Promise promise); void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); @@ -66,6 +58,7 @@ public slots: signals: void loadCompleted(); + void recoveryCompleted(); protected: /// Implements generic processing behavior for this thread. @@ -86,6 +79,9 @@ private: std::vector _backupHandlers; int _persistInterval { 0 }; + std::atomic _isRecovering { false }; + QString _recoveryFilename { }; + int64_t _lastCheck { 0 }; std::vector _backupRules; }; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index b91e12a9cf..fe145b341b 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -307,10 +307,7 @@ DomainServer::DomainServer(int argc, char* argv[]) : _contentManager->initialize(true); - qDebug() << "Existing backups:"; - for (auto& backup : _contentManager->getAllBackups()) { - qDebug() << " Backup: " << backup.name << backup.createdAt; - } + connect(_contentManager.get(), &DomainContentBackupManager::recoveryCompleted, this, &DomainServer::restart); } void DomainServer::parseCommandLine() { @@ -2131,24 +2128,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - QJsonObject rootJSON; - QJsonArray backupsJSON; + auto deferred = makePromise("getAllBackupInformation"); + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonDocument docJSON(QJsonObject::fromVariantMap(result)); - auto backups = _contentManager->getAllBackups(); - - for (const auto& backup : backups) { - QJsonObject obj; - obj["id"] = backup.id; - obj["name"] = backup.name; - obj["createdAtMillis"] = backup.createdAt.toMSecsSinceEpoch(); - obj["isManualBackup"] = backup.isManualBackup; - backupsJSON.push_back(obj); - } - - rootJSON["backups"] = backupsJSON; - QJsonDocument docJSON(rootJSON); - - connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); + }); + _contentManager->getAllBackupInformation(deferred); return true; } else if (url.path().startsWith(URI_API_BACKUPS_ID)) { auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); From 1120b12b8cb758578e16912961744a81a12b1880 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 15 Feb 2018 15:58:39 -0800 Subject: [PATCH 040/157] Fix argument to isAvailable --- domain-server/src/DomainContentBackupManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 54ea7e23d5..eccc3e904d 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -322,7 +322,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr for (auto& handler : _backupHandlers) { bool handlerIsAvailable { false }; float progress { 0.0f }; - //std::tie = handler->isAvailable(); + //std::tie = handler->isAvailable(fileInfo.absoluteFilePath()); //isAvailable = isAvailable && !handlerIsAvailable); //availabilityProgress += progress / _backupHandlers.size(); } From a7ca5398990a9328856f1a1aca75e4397a78db3a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Wed, 14 Feb 2018 16:48:37 -0800 Subject: [PATCH 041/157] Simplify BackupHandler pattern --- domain-server/src/BackupHandler.h | 1 - 1 file changed, 1 deletion(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 8599dafb29..960dde9b45 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -26,7 +26,6 @@ public: virtual void deleteBackup(QuaZip& zip) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; - using BackupHandlerPointer = std::unique_ptr; #endif /* hifi_BackupHandler_h */ From b76e1b9750973416aa2a316e601e2c36057b829e Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 15:46:35 -0800 Subject: [PATCH 042/157] Add backup status getters --- domain-server/src/AssetsBackupHandler.cpp | 51 +++++++++++++++++++---- domain-server/src/AssetsBackupHandler.h | 21 +++++----- domain-server/src/BackupHandler.h | 5 +++ domain-server/src/EntitiesBackupHandler.h | 13 +++--- 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index ae9cb58343..e683c626ea 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -111,6 +111,42 @@ void AssetsBackupHandler::checkForAssetsToDelete() { } } + +std::pair AssetsBackupHandler::isAvailable(QString filePath) { + auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { + return value.filePath == filePath; + }); + if (it == end(_backups)) { + return { true, 1.0f }; + } + + float progress = (float)it->mappings.size(); + for (const auto& mapping : it->mappings) { + if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { + progress -= 1.0f; + } + } + progress /= (float)it->mappings.size(); + + return { false, progress }; +} + +std::pair AssetsBackupHandler::getRecoveryStatus() { + if (_assetsLeftToUpload.empty() && + _mappingsLeftToSet.empty() && + _mappingsLeftToDelete.empty() && + _mappingRequestsInFlight == 0) { + return { false, 1.0f }; + } + + float progress = (float)_numRestoreOperations; + progress -= (float)_assetsLeftToUpload.size(); + progress -= (float)_mappingRequestsInFlight; + progress /= (float)_numRestoreOperations; + + return { true, progress }; +} + void AssetsBackupHandler::loadBackup(QuaZip& zip) { Q_ASSERT(QThread::currentThread() == thread()); @@ -451,6 +487,11 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } + _numRestoreOperations = _assetsLeftToUpload.size() + _mappingsLeftToSet.size(); + if (!_mappingsLeftToDelete.empty()) { + ++_numRestoreOperations; + } + qCDebug(asset_backup) << "Mappings to set:" << _mappingsLeftToSet.size(); qCDebug(asset_backup) << "Mappings to del:" << _mappingsLeftToDelete.size(); qCDebug(asset_backup) << "Assets to upload:" << _assetsLeftToUpload.size(); @@ -461,8 +502,6 @@ void AssetsBackupHandler::restoreAllAssets() { } void AssetsBackupHandler::restoreNextAsset() { - startOperation(); - if (_assetsLeftToUpload.empty()) { updateMappings(); return; @@ -500,9 +539,7 @@ void AssetsBackupHandler::updateMappings() { qCCritical(asset_backup) << " Error:" << request->getErrorString(); } - if (--_mappingRequestsInFlight == 0) { - stopOperation(); - } + --_mappingRequestsInFlight; request->deleteLater(); }); @@ -519,9 +556,7 @@ void AssetsBackupHandler::updateMappings() { qCCritical(asset_backup) << " Error:" << request->getErrorString(); } - if (--_mappingRequestsInFlight == 0) { - stopOperation(); - } + --_mappingRequestsInFlight; request->deleteLater(); }); diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 2ef454998e..1421ddd400 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -31,13 +31,16 @@ class AssetsBackupHandler : public QObject, public BackupHandlerInterface { public: AssetsBackupHandler(const QString& backupDirectory); - void loadBackup(QuaZip& zip); - void createBackup(QuaZip& zip); - void recoverBackup(QuaZip& zip); - void deleteBackup(QuaZip& zip); - void consolidateBackup(QuaZip& zip); + std::pair isAvailable(QString filePath) override; + std::pair getRecoveryStatus() override; - bool operationInProgress() const { return _operationInProgress; } + void loadBackup(QuaZip& zip) override; + void createBackup(QuaZip& zip) override; + void recoverBackup(QuaZip& zip) override; + void deleteBackup(QuaZip& zip) override; + void consolidateBackup(QuaZip& zip) override; + + bool operationInProgress() { return getRecoveryStatus().first; } private: void setupRefreshTimer(); @@ -48,9 +51,6 @@ private: void checkForMissingAssets(); void checkForAssetsToDelete(); - void startOperation() { _operationInProgress = true; } - void stopOperation() { _operationInProgress = false; } - void downloadMissingFiles(const AssetUtils::Mappings& mappings); void downloadNextMissingFile(); bool writeAssetFile(const AssetUtils::AssetHash& hash, const QByteArray& data); @@ -73,8 +73,6 @@ private: bool corruptedBackup; }; - bool _operationInProgress { false }; - // Internal storage for backups on disk bool _allBackupsLoadedSuccessfully { false }; std::vector _backups; @@ -89,6 +87,7 @@ private: std::vector> _mappingsLeftToSet; AssetUtils::AssetPathList _mappingsLeftToDelete; int _mappingRequestsInFlight { 0 }; + int _numRestoreOperations { 0 }; // Used to compute a restore progress. }; #endif /* hifi_AssetsBackupHandler_h */ diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 960dde9b45..d513820000 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -20,6 +20,11 @@ class BackupHandlerInterface { public: virtual ~BackupHandlerInterface() = default; + virtual std::pair isAvailable(QString filePath) = 0; + + // Returns whether a recovery is ongoing and a progress between 0 and 1 if one is. + virtual std::pair getRecoveryStatus() = 0; + virtual void loadBackup(QuaZip& zip) = 0; virtual void createBackup(QuaZip& zip) = 0; virtual void recoverBackup(QuaZip& zip) = 0; diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 6f66483a87..4cff7b6a33 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -20,19 +20,22 @@ class EntitiesBackupHandler : public BackupHandlerInterface { public: EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath); - void loadBackup(QuaZip& zip) {} + std::pair isAvailable(QString filePath) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } + + void loadBackup(QuaZip& zip) override {} // Create a skeleton backup - void createBackup(QuaZip& zip); + void createBackup(QuaZip& zip) override; // Recover from a full backup - void recoverBackup(QuaZip& zip); + void recoverBackup(QuaZip& zip) override; // Delete a skeleton backup - void deleteBackup(QuaZip& zip) {} + void deleteBackup(QuaZip& zip) override {} // Create a full backup - void consolidateBackup(QuaZip& zip) {} + void consolidateBackup(QuaZip& zip) override {} private: QString _entitiesFilePath; From 57410e4f1cc017c5aa23220fa96f6a445899d62a Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:22:06 -0800 Subject: [PATCH 043/157] Remove ES restart after restore --- domain-server/src/EntitiesBackupHandler.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/domain-server/src/EntitiesBackupHandler.cpp b/domain-server/src/EntitiesBackupHandler.cpp index deb92ee0f6..6ad00d01c8 100644 --- a/domain-server/src/EntitiesBackupHandler.cpp +++ b/domain-server/src/EntitiesBackupHandler.cpp @@ -16,7 +16,6 @@ #include #include -#include #include EntitiesBackupHandler::EntitiesBackupHandler(QString entitiesFilePath, QString entitiesReplacementFilePath) : @@ -72,13 +71,5 @@ void EntitiesBackupHandler::recoverBackup(QuaZip& zip) { if (entitiesFile.open(QIODevice::WriteOnly)) { entitiesFile.write(data.toGzippedByteArray()); - entitiesFile.close(); - - auto nodeList = DependencyManager::get(); - nodeList->eachMatchingNode([](const SharedNodePointer& otherNode) -> bool { - return otherNode->getType() == NodeType::EntityServer; - }, [nodeList](const SharedNodePointer& otherNode) { - QMetaObject::invokeMethod(nodeList.data(), "killNodeWithUUID", Q_ARG(const QUuid&, otherNode->getUUID())); - }); } } From 771e4cd9f4d5e8324813f41011717ea423327525 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:45:29 -0800 Subject: [PATCH 044/157] Hook up status and progress --- .../src/DomainContentBackupManager.cpp | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index eccc3e904d..37a8ecfce2 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -148,18 +148,15 @@ bool DomainContentBackupManager::process() { if (sinceLastSave > intervalToCheck) { _lastCheck = now; if (_isRecovering) { - bool anyHandlerIsRecovering { false }; - for (auto& handler : _backupHandlers) { - bool handlerIsRecovering { false }; - float progress { 0.0f }; - //std::tie = handler->getRecoveryStatus(); - if (handlerIsRecovering) { - anyHandlerIsRecovering = true; - emit recoveryCompleted(); - break; - } + using Value = std::vector::value_type; + bool isStillRecovering = std::any_of(begin(_backupHandlers), end(_backupHandlers), [](const Value& handler) { + return handler->getRecoveryStatus().first; + }); + + if (!isStillRecovering) { + _isRecovering = false; + emit recoveryCompleted(); } - _isRecovering = anyHandlerIsRecovering; } if (!_isRecovering) { @@ -320,11 +317,11 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr bool isAvailable { true }; float availabilityProgress { 0.0f }; for (auto& handler : _backupHandlers) { - bool handlerIsAvailable { false }; + bool handlerIsAvailable { true }; float progress { 0.0f }; - //std::tie = handler->isAvailable(fileInfo.absoluteFilePath()); - //isAvailable = isAvailable && !handlerIsAvailable); - //availabilityProgress += progress / _backupHandlers.size(); + std::tie(handlerIsAvailable, progress) = handler->isAvailable(fileInfo.absoluteFilePath()); + isAvailable &= handlerIsAvailable; + availabilityProgress += progress / _backupHandlers.size(); } backups.push_back(QVariantMap({ @@ -342,9 +339,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr bool isRecovering = _isRecovering.load(); if (_isRecovering) { for (auto& handler : _backupHandlers) { - bool handlerIsRecovering { false }; - float progress { 0.0f }; - //std::tie = handler->getRecoveryStatus(); + float progress = handler->getRecoveryStatus().second; recoveryProgress += progress / _backupHandlers.size(); } } From cae3e0a9dcceff2e182a012a6381743ca85905ba Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 16:59:17 -0800 Subject: [PATCH 045/157] Add status func to ContentSettingsBackupHandler --- domain-server/src/ContentSettingsBackupHandler.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h index 932b7c0c3f..8a81392513 100644 --- a/domain-server/src/ContentSettingsBackupHandler.h +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -19,15 +19,18 @@ class ContentSettingsBackupHandler : public BackupHandlerInterface { public: ContentSettingsBackupHandler(DomainServerSettingsManager& domainServerSettingsManager); - void loadBackup(QuaZip& zip) {}; + std::pair isAvailable(QString filePath) override { return { true, 1.0f }; } + std::pair getRecoveryStatus() override { return { false, 1.0f }; } - void createBackup(QuaZip& zip); + void loadBackup(QuaZip& zip) override {} - void recoverBackup(QuaZip& zip); + void createBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) {}; + void recoverBackup(QuaZip& zip) override; - void consolidateBackup(QuaZip& zip) {}; + void deleteBackup(QuaZip& zip) override {} + + void consolidateBackup(QuaZip& zip) override {} private: DomainServerSettingsManager& _settingsManager; }; From 2b85634a21abec80f8f689013c2696afb19e2dc9 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 17:03:55 -0800 Subject: [PATCH 046/157] Fix build error --- domain-server/src/BackupHandler.h | 2 ++ domain-server/src/EntitiesBackupHandler.h | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index d513820000..1bd40cd9e4 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -14,6 +14,8 @@ #include +#include + class QuaZip; class BackupHandlerInterface { diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 4cff7b6a33..1a6110f1cd 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -12,8 +12,6 @@ #ifndef hifi_EntitiesBackupHandler_h #define hifi_EntitiesBackupHandler_h -#include - #include "BackupHandler.h" class EntitiesBackupHandler : public BackupHandlerInterface { From 697f0c443cb0f3606ca2facc53dc35bd5ca98ae0 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 17:43:02 -0800 Subject: [PATCH 047/157] Fix warning --- domain-server/src/AssetsBackupHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index e683c626ea..db39f2a731 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -487,7 +487,7 @@ void AssetsBackupHandler::computeServerStateDifference(const AssetUtils::Mapping } } - _numRestoreOperations = _assetsLeftToUpload.size() + _mappingsLeftToSet.size(); + _numRestoreOperations = (int)_assetsLeftToUpload.size() + (int)_mappingsLeftToSet.size(); if (!_mappingsLeftToDelete.empty()) { ++_numRestoreOperations; } From f6e9d2c6dd05358d3053795f81705c43bbde02b3 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 18:16:30 -0800 Subject: [PATCH 048/157] Fix race condition in Asset Server --- assignment-client/src/assets/AssetServer.cpp | 28 ++++++++++++++++---- assignment-client/src/assets/AssetServer.h | 5 +++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp index 0be557bccd..4c6cba2e74 100644 --- a/assignment-client/src/assets/AssetServer.cpp +++ b/assignment-client/src/assets/AssetServer.cpp @@ -416,9 +416,9 @@ void AssetServer::completeSetup() { PathUtils::removeTemporaryApplicationDirs(); PathUtils::removeTemporaryApplicationDirs("Oven"); - // We're fully setup, remove the request queueing and replay all requests + qCDebug(asset_server) << "Overriding temporary queuing packet handler."; + // We're fully setup, override the request queueing handler and replay all requests auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); - packetReceiver.unregisterListener(this); packetReceiver.registerListener(PacketType::AssetGet, this, "handleAssetGet"); packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo"); packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload"); @@ -428,11 +428,30 @@ void AssetServer::completeSetup() { } void AssetServer::queueRequests(QSharedPointer packet, SharedNodePointer senderNode) { + qCDebug(asset_server) << "Queuing requests until fully setup"; + + QMutexLocker lock { &_queuedRequestsMutex }; _queuedRequests.push_back({ packet, senderNode }); + + // If we've stopped queueing but the callback was already in flight, + // then replay it immediately. + if (!_isQueueingRequests) { + lock.unlock(); + replayRequests(); + } } void AssetServer::replayRequests() { - for (const auto& request : _queuedRequests) { + RequestQueue queue; + { + QMutexLocker lock { &_queuedRequestsMutex }; + qSwap(queue, _queuedRequests); + _isQueueingRequests = false; + } + + qCDebug(asset_server) << "Replaying" << queue.size() << "requests."; + + for (const auto& request : queue) { switch (request.first->getType()) { case PacketType::AssetGet: handleAssetGet(request.first, request.second); @@ -447,11 +466,10 @@ void AssetServer::replayRequests() { handleAssetMappingOperation(request.first, request.second); break; default: - qWarning() << "Unknown queued request type:" << request.first->getType(); + qCWarning(asset_server) << "Unknown queued request type:" << request.first->getType(); break; } } - _queuedRequests.clear(); } void AssetServer::cleanupUnmappedFiles() { diff --git a/assignment-client/src/assets/AssetServer.h b/assignment-client/src/assets/AssetServer.h index b8aac800ed..c85fb89175 100644 --- a/assignment-client/src/assets/AssetServer.h +++ b/assignment-client/src/assets/AssetServer.h @@ -123,7 +123,10 @@ private: QHash> _pendingBakes; QThreadPool _bakingTaskPool; - QVector, SharedNodePointer>> _queuedRequests; + QMutex _queuedRequestsMutex; + bool _isQueueingRequests { true }; + using RequestQueue = QVector, SharedNodePointer>>; + RequestQueue _queuedRequests; bool _wasColorTextureCompressionEnabled { false }; bool _wasGrayscaleTextureCompressionEnabled { false }; From b30f98d5414759a1eae88505812cb481bb813844 Mon Sep 17 00:00:00 2001 From: Atlante45 Date: Thu, 15 Feb 2018 18:20:14 -0800 Subject: [PATCH 049/157] CR --- domain-server/src/DomainContentBackupManager.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 37a8ecfce2..5eb4e7627f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -503,8 +503,6 @@ void DomainContentBackupManager::consolidateBackup(MiniPromise::Promise promise, return; } - qDebug() << "copyFilePath" << copyFilePath; - promise->resolve({ { "success", true }, { "backupFilePath", copyFilePath } From 4bb8435ef884e760ca7374d25574a115ce1fa488 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 19:14:14 -0800 Subject: [PATCH 050/157] don't overwrite general description object with filtered one --- .../src/DomainServerSettingsManager.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 5f71890898..85d6a046b5 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1199,7 +1199,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { - + if (thread() != QThread::currentThread()) { bool success; @@ -1210,8 +1210,8 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings return success; } - QJsonArray& filteredDescriptionArray = settingsType == DomainSettings - ? _domainSettingsDescription : _contentSettingsDescription; + QJsonArray* filteredDescriptionArray = settingsType == DomainSettings + ? &_domainSettingsDescription : &_contentSettingsDescription; // grab a copy of the current config before restore, so that we can back out if something bad happens during QVariantMap preRestoreConfig = _configMap.getConfig(); @@ -1220,7 +1220,7 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings // enumerate through the settings in the description // if we have one in the restore then use it, otherwise clear it from current settings - foreach(const QJsonValue& descriptionGroupValue, filteredDescriptionArray) { + foreach(const QJsonValue& descriptionGroupValue, *filteredDescriptionArray) { QJsonObject descriptionGroupObject = descriptionGroupValue.toObject(); QString groupKey = descriptionGroupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray descriptionGroupSettings = descriptionGroupObject[DESCRIPTION_SETTINGS_KEY].toArray(); @@ -1356,15 +1356,15 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; // only enumerate the requested settings type (domain setting or content setting) - QJsonArray& filteredDescriptionArray = _descriptionArray; + QJsonArray* filteredDescriptionArray = &_descriptionArray; if (includeDomainSettings && !includeContentSettings) { - filteredDescriptionArray = _domainSettingsDescription; + filteredDescriptionArray = &_domainSettingsDescription; } else if (includeContentSettings && !includeDomainSettings) { - filteredDescriptionArray = _contentSettingsDescription; + filteredDescriptionArray = &_contentSettingsDescription; } // enumerate the groups in the potentially filtered object to find which settings to pass - foreach(const QJsonValue& groupValue, filteredDescriptionArray) { + foreach(const QJsonValue& groupValue, *filteredDescriptionArray) { QJsonObject groupObject = groupValue.toObject(); QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray(); From 29349d7bb2c198e859038c542af794161e5cba16 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 19:18:51 -0800 Subject: [PATCH 051/157] fix for isAvailable boolean in AssetsBackupHandler --- domain-server/src/AssetsBackupHandler.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index db39f2a731..c365b942af 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -120,13 +120,20 @@ std::pair AssetsBackupHandler::isAvailable(QString filePath) { return { true, 1.0f }; } - float progress = (float)it->mappings.size(); + int mappingsMissing = 0; for (const auto& mapping : it->mappings) { if (_assetsLeftToRequest.find(mapping.second) != end(_assetsLeftToRequest)) { - progress -= 1.0f; + ++mappingsMissing; } } - progress /= (float)it->mappings.size(); + + if (mappingsMissing == 0) { + return { true, 1.0f }; + } + + float progress = (float)it->mappings.size(); + progress -= (float)mappingsMissing; + progress /= it->mappings.size(); return { false, progress }; } From efa55c0a63d09b4f484c9248c59854924ba56b4e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:07:17 -0800 Subject: [PATCH 052/157] Update backup delete to not break rolling backups and remove unused asset files --- domain-server/src/AssetsBackupHandler.cpp | 6 +++--- domain-server/src/AssetsBackupHandler.h | 2 +- domain-server/src/BackupHandler.h | 2 +- .../src/ContentSettingsBackupHandler.h | 2 +- .../src/DomainContentBackupManager.cpp | 19 ++++++++++++++++--- .../src/DomainContentBackupManager.h | 1 + domain-server/src/EntitiesBackupHandler.h | 2 +- 7 files changed, 24 insertions(+), 10 deletions(-) diff --git a/domain-server/src/AssetsBackupHandler.cpp b/domain-server/src/AssetsBackupHandler.cpp index c365b942af..694277910f 100644 --- a/domain-server/src/AssetsBackupHandler.cpp +++ b/domain-server/src/AssetsBackupHandler.cpp @@ -306,7 +306,7 @@ void AssetsBackupHandler::recoverBackup(QuaZip& zip) { restoreAllAssets(); } -void AssetsBackupHandler::deleteBackup(QuaZip& zip) { +void AssetsBackupHandler::deleteBackup(const QString& absoluteFilePath) { Q_ASSERT(QThread::currentThread() == thread()); if (operationInProgress()) { @@ -315,10 +315,10 @@ void AssetsBackupHandler::deleteBackup(QuaZip& zip) { } auto it = find_if(begin(_backups), end(_backups), [&](const std::vector::value_type& value) { - return value.filePath == zip.getZipName(); + return value.filePath == absoluteFilePath; }); if (it == end(_backups)) { - qCDebug(asset_backup) << "Could not find backup" << zip.getZipName() << "to delete."; + qCDebug(asset_backup) << "Could not find backup" << absoluteFilePath << "to delete."; return; } diff --git a/domain-server/src/AssetsBackupHandler.h b/domain-server/src/AssetsBackupHandler.h index 1421ddd400..a4b62f563d 100644 --- a/domain-server/src/AssetsBackupHandler.h +++ b/domain-server/src/AssetsBackupHandler.h @@ -37,7 +37,7 @@ public: void loadBackup(QuaZip& zip) override; void createBackup(QuaZip& zip) override; void recoverBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) override; + void deleteBackup(const QString& absoluteFilePath) override; void consolidateBackup(QuaZip& zip) override; bool operationInProgress() { return getRecoveryStatus().first; } diff --git a/domain-server/src/BackupHandler.h b/domain-server/src/BackupHandler.h index 1bd40cd9e4..7d876cec01 100644 --- a/domain-server/src/BackupHandler.h +++ b/domain-server/src/BackupHandler.h @@ -30,7 +30,7 @@ public: virtual void loadBackup(QuaZip& zip) = 0; virtual void createBackup(QuaZip& zip) = 0; virtual void recoverBackup(QuaZip& zip) = 0; - virtual void deleteBackup(QuaZip& zip) = 0; + virtual void deleteBackup(const QString& absoluteFilePath) = 0; virtual void consolidateBackup(QuaZip& zip) = 0; }; using BackupHandlerPointer = std::unique_ptr; diff --git a/domain-server/src/ContentSettingsBackupHandler.h b/domain-server/src/ContentSettingsBackupHandler.h index 8a81392513..ba252c862c 100644 --- a/domain-server/src/ContentSettingsBackupHandler.h +++ b/domain-server/src/ContentSettingsBackupHandler.h @@ -28,7 +28,7 @@ public: void recoverBackup(QuaZip& zip) override; - void deleteBackup(QuaZip& zip) override {} + void deleteBackup(const QString& absoluteFilePath) override {} void consolidateBackup(QuaZip& zip) override {} private: diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 5eb4e7627f..bf388ce63e 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -89,8 +89,7 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { } auto name = obj["Name"].toString(); - auto format = obj["format"].toString(); - format = name.replace(" ", "_").toLower(); + auto format = name.replace(" ", "_").toLower(); qCDebug(domain_server) << " Name:" << name; qCDebug(domain_server) << " format:" << format; @@ -116,6 +115,12 @@ void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { } } +void DomainContentBackupManager::refreshBackupRules() { + for (auto& backup : _backupRules) { + backup.lastBackupSeconds = getMostRecentBackupTimeInSecs(backup.extensionFormat); + } +} + int64_t DomainContentBackupManager::getMostRecentBackupTimeInSecs(const QString& format) { int64_t mostRecentBackupInSecs = 0; @@ -235,8 +240,16 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons } QDir backupDir { _backupDirectory }; - QFile backupFile { backupDir.filePath(backupName) }; + auto absoluteFilePath { backupDir.filePath(backupName) }; + QFile backupFile { absoluteFilePath }; auto success = backupFile.remove(); + + refreshBackupRules(); + + for (auto& handler : _backupHandlers) { + handler->deleteBackup(absoluteFilePath); + } + promise->resolve({ { "success", success } }); diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 790dff0fb4..4ec3d7bcc7 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -68,6 +68,7 @@ protected: void load(); void backup(); void removeOldBackupVersions(const BackupRule& rule); + void refreshBackupRules(); bool getMostRecentBackup(const QString& format, QString& mostRecentBackupFileName, QDateTime& mostRecentBackupTime); int64_t getMostRecentBackupTimeInSecs(const QString& format); void parseSettings(const QJsonObject& settings); diff --git a/domain-server/src/EntitiesBackupHandler.h b/domain-server/src/EntitiesBackupHandler.h index 1a6110f1cd..c143fe5774 100644 --- a/domain-server/src/EntitiesBackupHandler.h +++ b/domain-server/src/EntitiesBackupHandler.h @@ -30,7 +30,7 @@ public: void recoverBackup(QuaZip& zip) override; // Delete a skeleton backup - void deleteBackup(QuaZip& zip) override {} + void deleteBackup(const QString& absoluteFilePath) override {} // Create a full backup void consolidateBackup(QuaZip& zip) override {} From bb8caa0ce3edccec0ed6d6d31c06f20643d4e508 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:07:45 -0800 Subject: [PATCH 053/157] Update HTTPConnection to not use QIODevice when given a QByteArray --- .../embedded-webserver/src/HTTPConnection.cpp | 72 ++++++++++--------- .../embedded-webserver/src/HTTPConnection.h | 1 + 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 6d0126b3d1..3f6f8d64ee 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -133,57 +133,33 @@ QList HTTPConnection::parseFormData() const { } void HTTPConnection::respond(const char* code, const QByteArray& content, const char* contentType, const Headers& headers) { - QByteArray data(content); - auto device { std::unique_ptr(new QBuffer()) }; - device->setBuffer(new QByteArray(content)); - if (device->open(QIODevice::ReadOnly)) { - respond(code, std::move(device), contentType, headers); - } else { - qCritical() << "Error opening QBuffer to respond to " << _requestUrl.path(); - } + respondWithStatusAndHeaders(code, contentType, headers, content.size()); + + _socket->write(content); + + _socket->disconnectFromHost(); + + // make sure we receive no further read notifications + disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } void HTTPConnection::respond(const char* code, std::unique_ptr device, const char* contentType, const Headers& headers) { _responseDevice = std::move(device); - _socket->write("HTTP/1.1 "); - if (_responseDevice->isSequential()) { qWarning() << "Error responding to HTTPConnection: sequential IO devices not supported"; - _socket->write(StatusCode500); - _socket->write("\r\n"); + respondWithStatusAndHeaders(StatusCode500, contentType, headers, 0); _socket->disconnect(SIGNAL(readyRead()), this); _socket->disconnectFromHost(); return; } - _socket->write(code); - _socket->write("\r\n"); - - for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); - it != end; it++) { - _socket->write(it.key()); - _socket->write(": "); - _socket->write(it.value()); - _socket->write("\r\n"); - } - - int csize = _responseDevice->size(); - if (csize > 0) { - _socket->write("Content-Length: "); - _socket->write(QByteArray::number(csize)); - _socket->write("\r\n"); - - _socket->write("Content-Type: "); - _socket->write(contentType); - _socket->write("\r\n"); - } - _socket->write("Connection: close\r\n\r\n"); + int totalToBeWritten = _responseDevice->size(); + respondWithStatusAndHeaders(code, contentType, headers, totalToBeWritten); if (_responseDevice->atEnd()) { _socket->disconnectFromHost(); } else { - int totalToBeWritten = csize; connect(_socket, &QTcpSocket::bytesWritten, this, [this, totalToBeWritten](size_t bytes) mutable { constexpr size_t HTTP_RESPONSE_CHUNK_SIZE = 1024 * 10; if (!_responseDevice->atEnd()) { @@ -201,6 +177,32 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } +void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t contentLength) { + _socket->write("HTTP/1.1 "); + + _socket->write(code); + _socket->write("\r\n"); + + for (Headers::const_iterator it = headers.constBegin(), end = headers.constEnd(); + it != end; it++) { + _socket->write(it.key()); + _socket->write(": "); + _socket->write(it.value()); + _socket->write("\r\n"); + } + + if (contentLength > 0) { + _socket->write("Content-Length: "); + _socket->write(QByteArray::number(contentLength)); + _socket->write("\r\n"); + + _socket->write("Content-Type: "); + _socket->write(contentType); + _socket->write("\r\n"); + } + _socket->write("Connection: close\r\n\r\n"); +} + void HTTPConnection::readRequest() { if (!_socket->canReadLine()) { return; diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index a020dfdca9..ec00864514 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -105,6 +105,7 @@ protected slots: void readContent (); protected: + void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t size); /// The parent HTTP manager HTTPManager* _parentManager; From ec3580f5964187fd898ce735e029544d57856970 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:08:57 -0800 Subject: [PATCH 054/157] Fix recovery ID not being recorded --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index bf388ce63e..8ea3d2ba90 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -281,6 +281,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, success = false; } else { _isRecovering = true; + _recoveryFilename = backupName; for (auto& handler : _backupHandlers) { handler->recoverBackup(zip); } From 0230abea790ddb7917ec366ae9b7b87c9038d3c4 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:54:35 -0800 Subject: [PATCH 055/157] Fix backup loading not getting auto backups --- .../src/DomainContentBackupManager.cpp | 108 +++++++++++------- .../src/DomainContentBackupManager.h | 13 ++- domain-server/src/DomainServer.cpp | 4 +- 3 files changed, 79 insertions(+), 46 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 8ea3d2ba90..db924c4e4f 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -300,12 +300,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } -void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise promise) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); - return; - } - +std::vector DomainContentBackupManager::getAllBackups() { QDir backupDir { _backupDirectory }; auto matchingFiles = backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, @@ -315,7 +310,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; - QVariantList backups; + std::vector backups; for (const auto& fileInfo : matchingFiles) { auto fileName = fileInfo.fileName(); @@ -338,17 +333,53 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr availabilityProgress += progress / _backupHandlers.size(); } - backups.push_back(QVariantMap({ - { "id", fileInfo.fileName() }, - { "name", name }, - { "createdAtMillis", createdAt.toMSecsSinceEpoch() }, - { "isAvailable", isAvailable }, - { "availabilityProgress", availabilityProgress }, - { "isManualBackup", type == MANUAL_BACKUP_PREFIX } - })); + backups.push_back( + { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }); } } + return backups; +} + +void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise promise) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + return; + } + + QDir backupDir { _backupDirectory }; + auto matchingFiles = + backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, + QDir::Files | QDir::NoSymLinks, QDir::Name); + QString prefixFormat = "(" + QRegExp::escape(AUTOMATIC_BACKUP_PREFIX) + "|" + QRegExp::escape(MANUAL_BACKUP_PREFIX) + ")"; + QString nameFormat = "(.+)"; + QString dateTimeFormat = "(" + DATETIME_FORMAT_RE + ")"; + QRegExp backupNameFormat { prefixFormat + nameFormat + "-" + dateTimeFormat + "\\.zip" }; + + auto backups = getAllBackups(); + + QVariantList variantBackups; + + for (auto& backup : backups) { + bool isAvailable { true }; + float availabilityProgress { 0.0f }; + for (auto& handler : _backupHandlers) { + bool handlerIsAvailable { true }; + float progress { 0.0f }; + std::tie(handlerIsAvailable, progress) = handler->isAvailable(backup.absolutePath); + isAvailable &= handlerIsAvailable; + availabilityProgress += progress / _backupHandlers.size(); + } + variantBackups.push_back(QVariantMap({ + { "id", backup.id }, + { "name", backup.name }, + { "createdAtMillis", backup.createdAt.toMSecsSinceEpoch() }, + { "isAvailable", isAvailable }, + { "availabilityProgress", availabilityProgress }, + { "isManualBackup", backup.isManualBackup } + })); + } + float recoveryProgress = 0.0f; bool isRecovering = _isRecovering.load(); if (_isRecovering) { @@ -365,7 +396,7 @@ void DomainContentBackupManager::getAllBackupInformation(MiniPromise::Promise pr }; QVariantMap info { - { "backups", backups }, + { "backups", variantBackups }, { "status", status } }; @@ -404,32 +435,27 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) } void DomainContentBackupManager::load() { - QDir backupDir { _backupDirectory }; - if (backupDir.exists()) { - - auto matchingFiles = backupDir.entryInfoList({ "backup-*.zip" }, QDir::Files | QDir::NoSymLinks, QDir::Name); - - for (const auto& file : matchingFiles) { - QFile backupFile { file.absoluteFilePath() }; - if (!backupFile.open(QIODevice::ReadOnly)) { - qCritical() << "Could not open file:" << file.absoluteFilePath(); - qCritical() << " ERROR:" << backupFile.errorString(); - continue; - } - - QuaZip zip { &backupFile }; - if (!zip.open(QuaZip::mdUnzip)) { - qCritical() << "Could not open backup archive:" << file.absoluteFilePath(); - qCritical() << " ERROR:" << zip.getZipError(); - continue; - } - - for (auto& handler : _backupHandlers) { - handler->loadBackup(zip); - } - - zip.close(); + auto backups = getAllBackups(); + for (auto& backup : backups) { + QFile backupFile{ backup.absolutePath }; + if (!backupFile.open(QIODevice::ReadOnly)) { + qCritical() << "Could not open file:" << backup.absolutePath; + qCritical() << " ERROR:" << backupFile.errorString(); + continue; } + + QuaZip zip{ &backupFile }; + if (!zip.open(QuaZip::mdUnzip)) { + qCritical() << "Could not open backup archive:" << backup.absolutePath; + qCritical() << " ERROR:" << zip.getZipError(); + continue; + } + + for (auto& handler : _backupHandlers) { + handler->loadBackup(zip); + } + + zip.close(); } } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 4ec3d7bcc7..2cfda4b650 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -24,6 +24,14 @@ #include +struct BackupItemInfo { + QString id; + QString name; + QString absolutePath; + QDateTime createdAt; + bool isManualBackup; +}; + class DomainContentBackupManager : public GenericThread { Q_OBJECT public: @@ -43,14 +51,13 @@ public: int persistInterval = DEFAULT_PERSIST_INTERVAL, bool debugTimestampNow = false); + std::vector getAllBackups(); void addBackupHandler(BackupHandlerPointer handler); - void aboutToFinish(); /// call this to inform the persist thread that the owner is about to finish to support final persist - void replaceData(QByteArray data); public slots: - void getAllBackupInformation(MiniPromise::Promise promise); + void getAllBackupsAndStatus(MiniPromise::Promise promise); void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index fe145b341b..157eaa483f 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2128,13 +2128,13 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == URI_API_BACKUPS) { - auto deferred = makePromise("getAllBackupInformation"); + auto deferred = makePromise("getAllBackupsAndStatus"); deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { QJsonDocument docJSON(QJsonObject::fromVariantMap(result)); connection->respond(HTTPConnection::StatusCode200, docJSON.toJson(), JSON_MIME_TYPE.toUtf8()); }); - _contentManager->getAllBackupInformation(deferred); + _contentManager->getAllBackupsAndStatus(deferred); return true; } else if (url.path().startsWith(URI_API_BACKUPS_ID)) { auto id = url.path().mid(QString(URI_API_BACKUPS_ID).length()); From 27c26bab8659b965229d451efb79edb6e4290615 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 13:57:38 -0800 Subject: [PATCH 056/157] Fix ambiguous int64_t in HTTPConnection --- libraries/embedded-webserver/src/HTTPConnection.cpp | 2 +- libraries/embedded-webserver/src/HTTPConnection.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/embedded-webserver/src/HTTPConnection.cpp b/libraries/embedded-webserver/src/HTTPConnection.cpp index 3f6f8d64ee..00879e1380 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.cpp +++ b/libraries/embedded-webserver/src/HTTPConnection.cpp @@ -177,7 +177,7 @@ void HTTPConnection::respond(const char* code, std::unique_ptr device disconnect(_socket, &QTcpSocket::readyRead, this, nullptr); } -void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t contentLength) { +void HTTPConnection::respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 contentLength) { _socket->write("HTTP/1.1 "); _socket->write(code); diff --git a/libraries/embedded-webserver/src/HTTPConnection.h b/libraries/embedded-webserver/src/HTTPConnection.h index ec00864514..60408d4325 100644 --- a/libraries/embedded-webserver/src/HTTPConnection.h +++ b/libraries/embedded-webserver/src/HTTPConnection.h @@ -105,7 +105,7 @@ protected slots: void readContent (); protected: - void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, int64_t size); + void respondWithStatusAndHeaders(const char* code, const char* contentType, const Headers& headers, qint64 size); /// The parent HTTP manager HTTPManager* _parentManager; From 936629ec1a41247b8ab47548b65921bd9ec7b081 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:05:40 -0800 Subject: [PATCH 057/157] Update DomainContentBackupManager to use emplace_back where available --- domain-server/src/DomainContentBackupManager.cpp | 6 +++--- domain-server/src/DomainContentBackupManager.h | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index db924c4e4f..618af15e60 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -333,8 +333,8 @@ std::vector DomainContentBackupManager::getAllBackups() { availabilityProgress += progress / _backupHandlers.size(); } - backups.push_back( - { fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, type == MANUAL_BACKUP_PREFIX }); + backups.emplace_back(fileInfo.fileName(), name, fileInfo.absoluteFilePath(), createdAt, + type == MANUAL_BACKUP_PREFIX); } } @@ -343,7 +343,7 @@ std::vector DomainContentBackupManager::getAllBackups() { void DomainContentBackupManager::getAllBackupsAndStatus(MiniPromise::Promise promise) { if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "getAllBackupInformation", Q_ARG(MiniPromise::Promise, promise)); + QMetaObject::invokeMethod(this, "getAllBackupsAndStatus", Q_ARG(MiniPromise::Promise, promise)); return; } diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 2cfda4b650..43e4cb16da 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -25,6 +25,13 @@ #include struct BackupItemInfo { + BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) + : id(pId) + , name(pName) + , absolutePath(pAbsolutePath) + , createdAt(pCreatedAt) + , isManualBackup(pIsManualBackup){}; + QString id; QString name; QString absolutePath; From 41d7d7efbbf4ec675f04a85e5a5f9f8e083f98a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:33:19 -0800 Subject: [PATCH 058/157] Fix initializer list style --- domain-server/src/DomainContentBackupManager.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index 43e4cb16da..f1aa4acab2 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -25,12 +25,8 @@ #include struct BackupItemInfo { - BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) - : id(pId) - , name(pName) - , absolutePath(pAbsolutePath) - , createdAt(pCreatedAt) - , isManualBackup(pIsManualBackup){}; + BackupItemInfo(QString pId, QString pName, QString pAbsolutePath, QDateTime pCreatedAt, bool pIsManualBackup) : + id(pId), name(pName), absolutePath(pAbsolutePath), createdAt(pCreatedAt), isManualBackup(pIsManualBackup) { }; QString id; QString name; From a2072062f18841f47d2ba31927d9cdaf96c23050 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 14:33:33 -0800 Subject: [PATCH 059/157] Fix recovery filename not being reset when recovery complete --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 618af15e60..dc749ad182 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -160,6 +160,7 @@ bool DomainContentBackupManager::process() { if (!isStillRecovering) { _isRecovering = false; + _recoveryFilename = ""; emit recoveryCompleted(); } } From f4cde44e6af1a4863d771ef6fc4ade79e4c04c32 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 16 Feb 2018 15:25:29 -0800 Subject: [PATCH 060/157] Fix indentation of brace initialization --- domain-server/src/DomainContentBackupManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index dc749ad182..a711d2112d 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -438,14 +438,14 @@ void DomainContentBackupManager::removeOldBackupVersions(const BackupRule& rule) void DomainContentBackupManager::load() { auto backups = getAllBackups(); for (auto& backup : backups) { - QFile backupFile{ backup.absolutePath }; + QFile backupFile { backup.absolutePath }; if (!backupFile.open(QIODevice::ReadOnly)) { qCritical() << "Could not open file:" << backup.absolutePath; qCritical() << " ERROR:" << backupFile.errorString(); continue; } - QuaZip zip{ &backupFile }; + QuaZip zip { &backupFile }; if (!zip.open(QuaZip::mdUnzip)) { qCritical() << "Could not open backup archive:" << backup.absolutePath; qCritical() << " ERROR:" << zip.getZipError(); From 29ceffd7cce43ba0c79e15a5ba2889df2b50671d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 12:05:23 -0800 Subject: [PATCH 061/157] add sections to content page for backup/restore --- .../resources/web/content/index.shtml | 2 + .../web/content/js/bootstrap-sortable.min.js | 1 + .../resources/web/content/js/content.js | 193 +++++++++++++++--- .../web/content/js/moment-locale.min.js | 1 + .../resources/web/css/bootstrap-sortable.css | 110 ++++++++++ domain-server/resources/web/css/style.css | 52 ++++- domain-server/resources/web/header.html | 1 + .../resources/web/js/base-settings.js | 8 +- .../resources/web/js/domain-server.js | 58 ++++-- domain-server/resources/web/js/shared.js | 6 +- .../resources/web/settings/js/settings.js | 18 +- 11 files changed, 387 insertions(+), 63 deletions(-) create mode 100755 domain-server/resources/web/content/js/bootstrap-sortable.min.js create mode 100644 domain-server/resources/web/content/js/moment-locale.min.js create mode 100755 domain-server/resources/web/css/bootstrap-sortable.css diff --git a/domain-server/resources/web/content/index.shtml b/domain-server/resources/web/content/index.shtml index 9b507f7826..f934faa976 100644 --- a/domain-server/resources/web/content/index.shtml +++ b/domain-server/resources/web/content/index.shtml @@ -14,6 +14,8 @@ + + diff --git a/domain-server/resources/web/content/js/bootstrap-sortable.min.js b/domain-server/resources/web/content/js/bootstrap-sortable.min.js new file mode 100755 index 0000000000..ac21ebe969 --- /dev/null +++ b/domain-server/resources/web/content/js/bootstrap-sortable.min.js @@ -0,0 +1 @@ +!function(t,e){"use strict";"function"==typeof define&&define.amd?define("tinysort",function(){return e}):t.tinysort=e}(this,function(){"use strict";function t(t,e){for(var r,a=t.length,o=a;o--;)e(t[r=a-o-1],r)}function e(t,e,r){for(var o in e)(r||t[o]===a)&&(t[o]=e[o]);return t}function r(t,e,r){f.push({prepare:t,sort:e,sortBy:r})}var a,o,n=!1,s=null,i=window,d=i.document,l=parseFloat,c=/(-?\d+\.?\d*)\s*$/g,u=/(\d+\.?\d*)\s*$/g,f=[],h=0,p=0,v=String.fromCharCode(4095),m={selector:s,order:"asc",attr:s,data:s,useVal:n,place:"org",returns:n,cases:n,natural:n,forceStrings:n,ignoreDashes:n,sortFunction:s,useFlex:n,emptyEnd:n};return i.Element&&((o=Element.prototype).matchesSelector=o.matchesSelector||o.mozMatchesSelector||o.msMatchesSelector||o.oMatchesSelector||o.webkitMatchesSelector||function(t){for(var e=(this.parentNode||this.document).querySelectorAll(t),r=-1;e[++r]&&e[r]!=this;);return!!e[r]}),e(r,{loop:t}),e(function(r,o){function i(t){var r=!!t.selector,a=r&&":"===t.selector[0],o=e(t||{},m);k.push(e({hasSelector:r,hasAttr:!(o.attr===s||""===o.attr),hasData:o.data!==s,hasFilter:a,sortReturnNumber:"asc"===o.order?1:-1},o))}function g(t,e,r){for(var a=r(t.toString()),o=r(e.toString()),n=0;a[n]&&o[n];n++)if(a[n]!==o[n]){var s=Number(a[n]),i=Number(o[n]);return s==a[n]&&i==o[n]?s-i:a[n]>o[n]?1:-1}return a.length-o.length}function b(t){for(var e,r,a=[],o=0,n=-1,s=0;e=(r=t.charAt(o++)).charCodeAt(0);){var i=46==e||e>=48&&57>=e;i!==s&&(a[++n]="",s=i),a[n]+=r}return a}function w(){return q.forEach(function(t){E.appendChild(t.elm)}),E}function y(t){var e=t.elm,r=d.createElement("div");return t.ghost=r,e.parentNode.insertBefore(r,e),t}function S(t,e){var r=t.ghost,a=r.parentNode;a.insertBefore(e,r),a.removeChild(r),delete t.ghost}function x(t,e){var r,a=t.elm;return e.selector&&(e.hasFilter?a.matchesSelector(e.selector)||(a=s):a=a.querySelector(e.selector)),e.hasAttr?r=a.getAttribute(e.attr):e.useVal?r=a.value||a.getAttribute("value"):e.hasData?r=a.getAttribute("data-"+e.data):a&&(r=a.textContent),C(r)&&(e.cases||(r=r.toLowerCase()),r=r.replace(/\s+/g," ")),null===r&&(r=v),r}function C(t){return"string"==typeof t}C(r)&&(r=d.querySelectorAll(r)),0===r.length&&console.warn("No elements to sort");var F,N,E=d.createDocumentFragment(),A=[],q=[],M=[],k=[],D=!0,z=r.length&&r[0].parentNode,Y=z.rootNode!==document,H=r.length&&(o===a||!1!==o.useFlex)&&!Y&&-1!==getComputedStyle(z,null).display.indexOf("flex");return function(){0===arguments.length?i({}):t(arguments,function(t){i(C(t)?{selector:t}:t)}),h=k.length}.apply(s,Array.prototype.slice.call(arguments,1)),t(r,function(t,e){N?N!==t.parentNode&&(D=!1):N=t.parentNode;var r=k[0],a=r.hasFilter,o=r.selector,n=!o||a&&t.matchesSelector(o)||o&&t.querySelector(o)?q:M,s={elm:t,pos:e,posn:n.length};A.push(s),n.push(s)}),F=q.slice(0),q.sort(function(e,r){var o=0;for(0!==p&&(p=0);0===o&&h>p;){var s=k[p],i=s.ignoreDashes?u:c;if(t(f,function(t){var e=t.prepare;e&&e(s)}),s.sortFunction)o=s.sortFunction(e,r);else if("rand"==s.order)o=Math.random()<.5?1:-1;else{var d=n,v=x(e,s),m=x(r,s),w=""===v||v===a,y=""===m||m===a;if(v===m)o=0;else if(s.emptyEnd&&(w||y))o=w&&y?0:w?1:-1;else{if(!s.forceStrings){var S=C(v)?v&&v.match(i):n,F=C(m)?m&&m.match(i):n;S&&F&&v.substr(0,v.length-S[0].length)==m.substr(0,m.length-F[0].length)&&(d=!n,v=l(S[0]),m=l(F[0]))}o=v===a||m===a?0:s.natural&&(isNaN(v)||isNaN(m))?g(v,m,b):m>v?-1:v>m?1:0}}t(f,function(t){var e=t.sort;e&&(o=e(s,d,v,m,o))}),0==(o*=s.sortReturnNumber)&&p++}return 0===o&&(o=e.pos>r.pos?1:-1),o}),function(){var t=q.length===A.length;if(D&&t)H?q.forEach(function(t,e){t.elm.style.order=e}):N?N.appendChild(w()):console.warn("parentNode has been removed");else{var e=k[0].place,r="start"===e,a="end"===e,o="first"===e,n="last"===e;if("org"===e)q.forEach(y),q.forEach(function(t,e){S(F[e],t.elm)});else if(r||a){var s=F[r?0:F.length-1],i=s&&s.elm.parentNode,d=i&&(r&&i.firstChild||i.lastChild);d&&(d!==s.elm&&(s={elm:d}),y(s),a&&i.appendChild(s.ghost),S(s,w()))}else(o||n)&&S(y(F[o?0:F.length-1]),w())}}(),q.map(function(t){return t.elm})},{plugin:r,defaults:m})}()),function(t,e){"function"==typeof define&&define.amd?define(["jquery","tinysort","moment"],e):e(t.jQuery,t.tinysort,t.moment||void 0)}(this,function(t,e,r){var a,o,n,s=t(document);function i(e){var s=void 0!==r;a=e.sign?e.sign:"arrow","default"==e.customSort&&(e.customSort=u),o=e.customSort||o||u,n=e.emptyEnd,t("table.sortable").each(function(){var a=t(this),o=!0===e.applyLast;a.find("span.sign").remove(),a.find("> thead [colspan]").each(function(){for(var e=parseFloat(t(this).attr("colspan")),r=1;r')}),a.find("> thead [rowspan]").each(function(){for(var e=t(this),r=parseFloat(e.attr("rowspan")),a=1;a')}}),a.find("> thead tr").each(function(e){t(this).find("th").each(function(r){var a=t(this);a.addClass("nosort").removeClass("up down"),a.attr("data-sortcolumn",r),a.attr("data-sortkey",r+"-"+e)})}),a.find("> thead .rowspan-compensate, .colspan-compensate").remove(),a.find("th").each(function(){var e=t(this);if(void 0!==e.attr("data-dateformat")&&s){var o=parseFloat(e.attr("data-sortcolumn"));a.find("td:nth-child("+(o+1)+")").each(function(){var a=t(this);a.attr("data-value",r(a.text(),e.attr("data-dateformat")).format("YYYY/MM/DD/HH/mm/ss"))})}else if(void 0!==e.attr("data-valueprovider")){o=parseFloat(e.attr("data-sortcolumn"));a.find("td:nth-child("+(o+1)+")").each(function(){var r=t(this);r.attr("data-value",new RegExp(e.attr("data-valueprovider")).exec(r.text())[0])})}}),a.find("td").each(function(){var e=t(this);void 0!==e.attr("data-dateformat")&&s?e.attr("data-value",r(e.text(),e.attr("data-dateformat")).format("YYYY/MM/DD/HH/mm/ss")):void 0!==e.attr("data-valueprovider")?e.attr("data-value",new RegExp(e.attr("data-valueprovider")).exec(e.text())[0]):void 0===e.attr("data-value")&&e.attr("data-value",e.text())});var n=c(a),i=n.bsSort;a.find('> thead th[data-defaultsort!="disabled"]').each(function(e){var r=t(this),a=r.closest("table.sortable");r.data("sortTable",a);var s=r.attr("data-sortkey"),d=o?n.lastSort:-1;i[s]=o?i[s]:r.attr("data-defaultsort"),void 0!==i[s]&&o===(s===d)&&(i[s]="asc"===i[s]?"desc":"asc",f(r,a))})})}function d(e){e.find("> tbody [rowspan]").each(function(){var e=t(this),r=parseFloat(e.attr("rowspan"));e.removeAttr("rowspan");var a=e.attr("rowspan-group")||function(){function t(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return t()+t()+"-"+t()+"-"+t()+"-"+t()+"-"+t()+t()+t()}();e.attr("rowspan-group",a),e.attr("rowspan-value",r);for(var o=e.parent("tr"),n=o.children().index(e),s=1;s thead th[data-defaultsort!="disabled"]').each(function(e){var a=t(this),o=a.attr("data-sortkey");r.bsSort[o]=a.attr("data-defaultsort"),void 0!==r.bsSort[o]&&(r.lastSort=o)}),e.data("bootstrap-sortable-context",r)),r}function u(t,r){e(t,r)}function f(e,r){r.trigger("before-sort"),d(r);var s=parseFloat(e.attr("data-sortcolumn")),i=c(r),l=i.bsSort;if(e.attr("colspan")){var u=parseFloat(e.data("mainsort"))||0,h=parseFloat(e.data("sortkey").split("-").pop());if(r.find("> thead tr").length-1>h)return void f(r.find('[data-sortkey="'+(s+u)+"-"+(h+1)+'"]'),r);s+=u}var p=e.attr("data-defaultsign")||a;if(r.find("> thead th").each(function(){t(this).removeClass("up").removeClass("down").addClass("nosort")}),t.browser.mozilla){var v=r.find("> thead div.mozilla");void 0!==v&&(v.find(".sign").remove(),v.parent().html(v.html())),e.wrapInner('
'),e.children().eq(0).append('')}else r.find("> thead span.sign").remove(),e.append('');var m=e.attr("data-sortkey"),g="desc"!==e.attr("data-firstsort")?"desc":"asc",b=l[m]||g;i.lastSort!==m&&void 0!==l[m]||(b="asc"===b?"desc":"asc"),l[m]=b,i.lastSort=m,"desc"===l[m]?(e.find("span.sign").addClass("up"),e.addClass("up").removeClass("down nosort")):e.addClass("down").removeClass("up nosort");var w=r.children("tbody").children("tr"),y=[];t(w.filter('[data-disablesort="true"]').get().reverse()).each(function(e,r){var a=t(r);y.push({index:w.index(a),row:a}),a.remove()});var S=w.not('[data-disablesort="true"]');if(0!=S.length){var x="asc"===l[m]&&n;o(S,{emptyEnd:x,selector:"td:nth-child("+(s+1)+")",order:l[m],data:"value"})}t(y.reverse()).each(function(t,e){0===e.index?r.children("tbody").prepend(e.row):r.children("tbody").children("tr").eq(e.index-1).after(e.row)}),r.find("> tbody > tr > td.sorted,> thead th.sorted").removeClass("sorted"),S.find("td:eq("+s+")").addClass("sorted"),e.addClass("sorted"),r.find("> tbody [rowspan-group]").each(function(){for(var e=t(this),r=e.attr("rowspan-group"),a=e.parent("tr"),o=a.children().index(e);;){var n=a.next("tr");if(!n.is("tr"))break;var s=n.children().eq(o);if(s.attr("rowspan-group")!==r)break;var i=parseFloat(e.attr("rowspan"))||1;e.attr("rowspan",i+1),s.remove(),a=n}}),r.trigger("sorted")}if(t.bootstrapSortable=function(t){null==t?i({}):t.constructor===Boolean?i({applyLast:t}):void 0!==t.sortingHeader?l(t.sortingHeader):i(t)},s.on("click",'table.sortable>thead th[data-defaultsort!="disabled"]',function(t){l(this)}),!t.browser){t.browser={chrome:!1,mozilla:!1,opera:!1,msie:!1,safari:!1};var h=navigator.userAgent;t.each(t.browser,function(e){t.browser[e]=!!new RegExp(e,"i").test(h),t.browser.mozilla&&"mozilla"===e&&(t.browser.mozilla=!!new RegExp("firefox","i").test(h)),t.browser.chrome&&"safari"===e&&(t.browser.safari=!1)})}t(t.bootstrapSortable)}); \ No newline at end of file diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index e448952c65..e2b653995f 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -1,37 +1,174 @@ $(document).ready(function(){ - Settings.afterReloadActions = function() {}; + var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; + var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; - var frm = $('#upload-form'); - frm.submit(function (ev) { - $.ajax({ - type: frm.attr('method'), - url: frm.attr('action'), - data: new FormData($(this)[0]), - cache: false, - contentType: false, - processData: false, - success: function (data) { - swal({ - title: 'Uploaded', - type: 'success', - text: 'Your Entity Server is restarting to replace its local content with the uploaded file.', - confirmButtonText: 'OK' - }) - }, - error: function (data) { - swal({ - title: '', - type: 'error', - text: 'Your entities file could not be transferred to the Entity Server.
Verify that the file is a .json or .json.gz entities file and try again.', - html: true, - confirmButtonText: 'OK', - }); + function setupBackupUpload() { + // construct the HTML needed for the settings backup panel + var html = "
"; + + html += "Upload a Content Backup to replace the content of this domain"; + html += "
Note: Your domain's content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
"; + + html += ""; + html += ""; + + html += "
"; + + $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); + } + + var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; + var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; + var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; + var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; + var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody'; + var automaticBackups = []; + var manualBackups = []; + + function setupContentArchives() { + + // construct the HTML needed for the content archives panel + var html = "
"; + html += ""; + html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your domain content and settings backups." + html += "
"; + html += ""; + + var backups_table_head = ""; + + html += backups_table_head; + html += "
Archive NameArchive DateActions
"; + html += "
"; + html += ""; + html += "You can generate and download an archive of your domain content right now. You can also download, delete and restore any archive listed."; + html += ""; + html += "
"; + html += ""; + html += backups_table_head; + html += "
"; + + // put the base HTML in the content archives panel + $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); + } + + function reloadLatestBackups() { + // make a GET request to get backup information to populate the table + $.get('/api/backups', function(data) { + // split the returned data into manual and automatic manual backups + var splitBackups = _.partition(data.backups, function(value, index) { + return value.isManualBackup; + }); + + manualBackups = splitBackups[0]; + automaticBackups = splitBackups[1]; + + // populate the backups tables with the backups + function createBackupTableRow(backup) { + return "" + backup.name + "" + + moment(backup.createdAtMillis).format('lll') + + "" + + "" + + ""; } + + var automaticRows = ""; + + if (automaticBackups.length > 0) { + for (var backupIndex in automaticBackups) { + // create a table row for this backup and add it to the rows we'll put in the table body + automaticRows += createBackupTableRow(automaticBackups[backupIndex]); + } + } + + $('#' + AUTOMATIC_ARCHIVES_TBODY_ID).html(automaticRows); + + var manualRows = ""; + + if (manualBackups.length > 0) { + for (var backupIndex in manualBackups) { + // create a table row for this backup and add it to the rows we'll put in the table body + manualRows += createBackupTableRow(manualBackups[backupIndex]); + } + } + + $('#' + MANUAL_ARCHIVES_TBODY_ID).html(manualRows); + + // tell bootstrap sortable to update for the new rows + $.bootstrapSortable({ applyLast: true }); + + }).fail(function(){ + // we've hit the very rare case where we couldn't load the list of backups from the domain server + + // set our backups to empty + automaticBackups = []; + manualBackups = []; + + // replace the content archives panel with a simple error message + // stating that the user should reload the page + $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html( + "
" + + "There was a problem loading your list of automatic and manual content archives. Please reload the page to try again." + + "
" + ); + + }).always(function(){ + // toggle showing or hiding the tables depending on if they have entries + $('#' + AUTOMATIC_ARCHIVES_TABLE_ID).toggle(automaticBackups.length > 0); + $('#' + MANUAL_ARCHIVES_TABLE_ID).toggle(manualBackups.length > 0); }); + } - ev.preventDefault(); + // handle click on manual archive creation button + $('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) { + e.preventDefault(); - showSpinnerAlert("Uploading Entities File"); + // show a sweet alert to ask the user to provide a name for their content archive + swal({ + title: "Generate a Content Archive", + type: "input", + text: "This will capture the state of all the content in your domain right now, which you can save as a backup and restore from later.", + confirmButtonText: "Generate Archive", + showCancelButton: true, + closeOnConfirm: false, + inputPlaceholder: 'Archive Name' + }, function(inputValue){ + if (inputValue === false) { + return false; + } + + if (inputValue === "") { + swal.showInputError("Please give the content archive a name.") + return false; + } + + // post the provided archive name to ask the server to kick off a manual backup + $.ajax({ + type: 'POST', + url: '/api/backup', + data: { + 'name': inputValue + } + }).done(function(data) { + // since we successfully setup a new content archive, reload the table of archives + // which should show that this archive is pending creation + reloadContentArchives(); + }).fail(function(jqXHR, textStatus, errorThrown) { + + }); + + swal.close(); + }); }); + + Settings.extraGroupsAtIndex = Settings.extraContentGroupsAtIndex; + + Settings.afterReloadActions = function() { + setupBackupUpload(); + setupContentArchives(); + + // load the latest backups immediately + reloadLatestBackups(); + }; }); diff --git a/domain-server/resources/web/content/js/moment-locale.min.js b/domain-server/resources/web/content/js/moment-locale.min.js new file mode 100644 index 0000000000..fabea0c841 --- /dev/null +++ b/domain-server/resources/web/content/js/moment-locale.min.js @@ -0,0 +1 @@ +!function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):e.moment=a()}(this,function(){"use strict";function e(){return Ea.apply(null,arguments)}function a(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function t(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function s(e){return void 0===e}function n(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function d(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function r(e,a){var t,s=[];for(t=0;t0)for(t=0;t=0?t?"+":"":"-")+Math.pow(10,Math.max(0,n)).toString().substr(1)+s}function j(e,a,t,s){var n=s;"string"==typeof s&&(n=function(){return this[s]()}),e&&(Va[e]=n),a&&(Va[a[0]]=function(){return b(n.apply(this,arguments),a[1],a[2])}),t&&(Va[t]=function(){return this.localeData().ordinal(n.apply(this,arguments),e)})}function x(e){return e.match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"")}function P(e,a){return e.isValid()?(a=O(a,e.localeData()),Ua[a]=Ua[a]||function(e){var a,t,s=e.match(Ca);for(a=0,t=s.length;a=0&&Ga.test(e);)e=e.replace(Ga,t),Ga.lastIndex=0,s-=1;return e}function W(e,a,t){ot[e]=D(a)?a:function(e,s){return e&&t?t:a}}function E(e,a){return _(ot,e)?ot[e](a._strict,a._locale):new RegExp(function(e){return A(e.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(e,a,t,s,n){return a||t||s||n}))}(e))}function A(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function F(e,a){var t,s=a;for("string"==typeof e&&(e=[e]),n(a)&&(s=function(e,t){t[a]=Y(e)}),t=0;t=0&&isFinite(a.getUTCFullYear())&&a.setUTCFullYear(e),a}function B(e,a,t){var s=7+a-t;return-((7+$(e,0,s).getUTCDay()-a)%7)+s-1}function q(e,a,t,s,n){var d,r,_=1+7*(a-1)+(7+t-s)%7+B(e,s,n);return _<=0?r=N(d=e-1)+_:_>N(e)?(d=e+1,r=_-N(e)):(d=e,r=_),{year:d,dayOfYear:r}}function Q(e,a,t){var s,n,d=B(e.year(),a,t),r=Math.floor((e.dayOfYear()-d-1)/7)+1;return r<1?s=r+X(n=e.year()-1,a,t):r>X(e.year(),a,t)?(s=r-X(e.year(),a,t),n=e.year()+1):(n=e.year(),s=r),{week:s,year:n}}function X(e,a,t){var s=B(e,a,t),n=B(e+1,a,t);return(N(e)-s+n)/7}function ee(){function e(e,a){return a.length-e.length}var a,t,s,n,d,r=[],_=[],i=[],m=[];for(a=0;a<7;a++)t=o([2e3,1]).day(a),s=this.weekdaysMin(t,""),n=this.weekdaysShort(t,""),d=this.weekdays(t,""),r.push(s),_.push(n),i.push(d),m.push(s),m.push(n),m.push(d);for(r.sort(e),_.sort(e),i.sort(e),m.sort(e),a=0;a<7;a++)_[a]=A(_[a]),i[a]=A(i[a]),m[a]=A(m[a]);this._weekdaysRegex=new RegExp("^("+m.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+_.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function ae(){return this.hours()%12||12}function te(e,a){j(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),a)})}function se(e,a){return a._meridiemParse}function ne(e){return e?e.toLowerCase().replace("_","-"):e}function de(e){var a=null;if(!At[e]&&"undefined"!=typeof module&&module&&module.exports)try{a=Ot._abbr;require("./locale/"+e),re(a)}catch(e){}return At[e]}function re(e,a){var t;return e&&(t=s(a)?ie(e):_e(e,a))&&(Ot=t),Ot._abbr}function _e(e,a){if(null!==a){var t=Et;if(a.abbr=e,null!=At[e])p("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),t=At[e]._config;else if(null!=a.parentLocale){if(null==At[a.parentLocale])return Ft[a.parentLocale]||(Ft[a.parentLocale]=[]),Ft[a.parentLocale].push({name:e,config:a}),null;t=At[a.parentLocale]._config}return At[e]=new g(T(t,a)),Ft[e]&&Ft[e].forEach(function(e){_e(e.name,e.config)}),re(e),At[e]}return delete At[e],null}function ie(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return Ot;if(!a(e)){if(t=de(e))return t;e=[e]}return function(e){for(var a,t,s,n,d=0;d0;){if(s=de(n.slice(0,a).join("-")))return s;if(t&&t.length>=a&&y(n,t,!0)>=a-1)break;a--}d++}return null}(e)}function oe(e){var a,t=e._a;return t&&-2===m(e).overflow&&(a=t[lt]<0||t[lt]>11?lt:t[Mt]<1||t[Mt]>U(t[ut],t[lt])?Mt:t[ht]<0||t[ht]>24||24===t[ht]&&(0!==t[Lt]||0!==t[ct]||0!==t[Yt])?ht:t[Lt]<0||t[Lt]>59?Lt:t[ct]<0||t[ct]>59?ct:t[Yt]<0||t[Yt]>999?Yt:-1,m(e)._overflowDayOfYear&&(aMt)&&(a=Mt),m(e)._overflowWeeks&&-1===a&&(a=yt),m(e)._overflowWeekday&&-1===a&&(a=ft),m(e).overflow=a),e}function me(e,a,t){return null!=e?e:null!=a?a:t}function ue(a){var t,s,n,d,r,_=[];if(!a._d){for(n=function(a){var t=new Date(e.now());return a._useUTC?[t.getUTCFullYear(),t.getUTCMonth(),t.getUTCDate()]:[t.getFullYear(),t.getMonth(),t.getDate()]}(a),a._w&&null==a._a[Mt]&&null==a._a[lt]&&function(e){var a,t,s,n,d,r,_,i;if(null!=(a=e._w).GG||null!=a.W||null!=a.E)d=1,r=4,t=me(a.GG,e._a[ut],Q(ye(),1,4).year),s=me(a.W,1),((n=me(a.E,1))<1||n>7)&&(i=!0);else{d=e._locale._week.dow,r=e._locale._week.doy;var o=Q(ye(),d,r);t=me(a.gg,e._a[ut],o.year),s=me(a.w,o.week),null!=a.d?((n=a.d)<0||n>6)&&(i=!0):null!=a.e?(n=a.e+d,(a.e<0||a.e>6)&&(i=!0)):n=d}s<1||s>X(t,d,r)?m(e)._overflowWeeks=!0:null!=i?m(e)._overflowWeekday=!0:(_=q(t,s,n,d,r),e._a[ut]=_.year,e._dayOfYear=_.dayOfYear)}(a),null!=a._dayOfYear&&(r=me(a._a[ut],n[ut]),(a._dayOfYear>N(r)||0===a._dayOfYear)&&(m(a)._overflowDayOfYear=!0),s=$(r,0,a._dayOfYear),a._a[lt]=s.getUTCMonth(),a._a[Mt]=s.getUTCDate()),t=0;t<3&&null==a._a[t];++t)a._a[t]=_[t]=n[t];for(;t<7;t++)a._a[t]=_[t]=null==a._a[t]?2===t?1:0:a._a[t];24===a._a[ht]&&0===a._a[Lt]&&0===a._a[ct]&&0===a._a[Yt]&&(a._nextDay=!0,a._a[ht]=0),a._d=(a._useUTC?$:function(e,a,t,s,n,d,r){var _=new Date(e,a,t,s,n,d,r);return e<100&&e>=0&&isFinite(_.getFullYear())&&_.setFullYear(e),_}).apply(null,_),d=a._useUTC?a._d.getUTCDay():a._d.getDay(),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[ht]=24),a._w&&void 0!==a._w.d&&a._w.d!==d&&(m(a).weekdayMismatch=!0)}}function le(e){var a,t,s,n,d,r,_=e._i,i=zt.exec(_)||Jt.exec(_);if(i){for(m(e).iso=!0,a=0,t=Rt.length;a0&&m(a).unusedInput.push(r),_=_.slice(_.indexOf(s)+s.length),o+=s.length),Va[d]?(s?m(a).empty=!1:m(a).unusedTokens.push(d),J(d,s,a)):a._strict&&!s&&m(a).unusedTokens.push(d);m(a).charsLeftOver=i-o,_.length>0&&m(a).unusedInput.push(_),a._a[ht]<=12&&!0===m(a).bigHour&&a._a[ht]>0&&(m(a).bigHour=void 0),m(a).parsedDateParts=a._a.slice(0),m(a).meridiem=a._meridiem,a._a[ht]=function(e,a,t){var s;if(null==t)return a;return null!=e.meridiemHour?e.meridiemHour(a,t):null!=e.isPM?((s=e.isPM(t))&&a<12&&(a+=12),s||12!==a||(a=0),a):a}(a._locale,a._a[ht],a._meridiem),ue(a),oe(a)}else he(a);else le(a)}function ce(_){var o=_._i,c=_._f;return _._locale=_._locale||ie(_._l),null===o||void 0===c&&""===o?l({nullInput:!0}):("string"==typeof o&&(_._i=o=_._locale.preparse(o)),L(o)?new h(oe(o)):(d(o)?_._d=o:a(c)?function(e){var a,t,s,n,d;if(0===e._f.length)return m(e).invalidFormat=!0,void(e._d=new Date(NaN));for(n=0;nd&&(a=d),function(e,a,t,s,n){var d=q(e,a,t,s,n),r=$(d.year,0,d.dayOfYear);return this.year(r.getUTCFullYear()),this.month(r.getUTCMonth()),this.date(r.getUTCDate()),this}.call(this,e,a,t,s,n))}function ze(e,a){a[Yt]=Y(1e3*("0."+e))}function Je(e){return e}function Ne(e,a,t,s){var n=ie(),d=o().set(s,a);return n[t](d,e)}function Re(e,a,t){if(n(e)&&(a=e,e=void 0),e=e||"",null!=a)return Ne(e,a,t,"month");var s,d=[];for(s=0;s<12;s++)d[s]=Ne(e,s,t,"month");return d}function Ie(e,a,t,s){"boolean"==typeof e?(n(a)&&(t=a,a=void 0),a=a||""):(t=a=e,e=!1,n(a)&&(t=a,a=void 0),a=a||"");var d=ie(),r=e?d._week.dow:0;if(null!=t)return Ne(a,(t+r)%7,s,"day");var _,i=[];for(_=0;_<7;_++)i[_]=Ne(a,(_+r)%7,s,"day");return i}function Ce(e,a,t,s){var n=He(a,t);return e._milliseconds+=s*n._milliseconds,e._days+=s*n._days,e._months+=s*n._months,e._bubble()}function Ge(e){return e<0?Math.floor(e):Math.ceil(e)}function Ue(e){return 4800*e/146097}function Ve(e){return 146097*e/4800}function Ke(e){return function(){return this.as(e)}}function Ze(e){return function(){return this.isValid()?this._data[e]:NaN}}function $e(e){return(e>0)-(e<0)||+e}function Be(){if(!this.isValid())return this.localeData().invalidDate();var e,a,t=vs(this._milliseconds)/1e3,s=vs(this._days),n=vs(this._months);a=c((e=c(t/60))/60),t%=60,e%=60;var d=c(n/12),r=n%=12,_=s,i=a,o=e,m=t?t.toFixed(3).replace(/\.?0+$/,""):"",u=this.asSeconds();if(!u)return"P0D";var l=u<0?"-":"",M=$e(this._months)!==$e(u)?"-":"",h=$e(this._days)!==$e(u)?"-":"",L=$e(this._milliseconds)!==$e(u)?"-":"";return l+"P"+(d?M+d+"Y":"")+(r?M+r+"M":"")+(_?h+_+"D":"")+(i||o||m?"T":"")+(i?L+i+"H":"")+(o?L+o+"M":"")+(m?L+m+"S":"")}function qe(e,a,t){return"m"===t?a?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443":"h"===t?a?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u0445\u0432\u0456\u043b\u0456\u043d\u0430_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d":"\u0445\u0432\u0456\u043b\u0456\u043d\u0443_\u0445\u0432\u0456\u043b\u0456\u043d\u044b_\u0445\u0432\u0456\u043b\u0456\u043d",hh:a?"\u0433\u0430\u0434\u0437\u0456\u043d\u0430_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d":"\u0433\u0430\u0434\u0437\u0456\u043d\u0443_\u0433\u0430\u0434\u0437\u0456\u043d\u044b_\u0433\u0430\u0434\u0437\u0456\u043d",dd:"\u0434\u0437\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u0437\u0451\u043d",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u044b_\u043c\u0435\u0441\u044f\u0446\u0430\u045e",yy:"\u0433\u043e\u0434_\u0433\u0430\u0434\u044b_\u0433\u0430\u0434\u043e\u045e"}[t],+e)}function Qe(e,a,t){return e+" "+function(e,a){if(2===a)return function(e){var a={m:"v",b:"v",d:"z"};if(void 0===a[e.charAt(0)])return e;return a[e.charAt(0)]+e.substring(1)}(e);return e}({mm:"munutenn",MM:"miz",dd:"devezh"}[t],e)}function Xe(e){return e>9?Xe(e%10):e}function ea(e,a,t){var s=e+" ";switch(t){case"ss":return s+=1===e?"sekunda":2===e||3===e||4===e?"sekunde":"sekundi";case"m":return a?"jedna minuta":"jedne minute";case"mm":return s+=1===e?"minuta":2===e||3===e||4===e?"minute":"minuta";case"h":return a?"jedan sat":"jednog sata";case"hh":return s+=1===e?"sat":2===e||3===e||4===e?"sata":"sati";case"dd":return s+=1===e?"dan":"dana";case"MM":return s+=1===e?"mjesec":2===e||3===e||4===e?"mjeseca":"mjeseci";case"yy":return s+=1===e?"godina":2===e||3===e||4===e?"godine":"godina"}}function aa(e){return e>1&&e<5&&1!=~~(e/10)}function ta(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"p\xe1r sekund":"p\xe1r sekundami";case"ss":return a||s?n+(aa(e)?"sekundy":"sekund"):n+"sekundami";break;case"m":return a?"minuta":s?"minutu":"minutou";case"mm":return a||s?n+(aa(e)?"minuty":"minut"):n+"minutami";break;case"h":return a?"hodina":s?"hodinu":"hodinou";case"hh":return a||s?n+(aa(e)?"hodiny":"hodin"):n+"hodinami";break;case"d":return a||s?"den":"dnem";case"dd":return a||s?n+(aa(e)?"dny":"dn\xed"):n+"dny";break;case"M":return a||s?"m\u011bs\xedc":"m\u011bs\xedcem";case"MM":return a||s?n+(aa(e)?"m\u011bs\xedce":"m\u011bs\xedc\u016f"):n+"m\u011bs\xedci";break;case"y":return a||s?"rok":"rokem";case"yy":return a||s?n+(aa(e)?"roky":"let"):n+"lety";break}}function sa(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function na(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function da(e,a,t,s){var n={m:["eine Minute","einer Minute"],h:["eine Stunde","einer Stunde"],d:["ein Tag","einem Tag"],dd:[e+" Tage",e+" Tagen"],M:["ein Monat","einem Monat"],MM:[e+" Monate",e+" Monaten"],y:["ein Jahr","einem Jahr"],yy:[e+" Jahre",e+" Jahren"]};return a?n[t][0]:n[t][1]}function ra(e,a,t,s){var n={s:["m\xf5ne sekundi","m\xf5ni sekund","paar sekundit"],ss:[e+"sekundi",e+"sekundit"],m:["\xfche minuti","\xfcks minut"],mm:[e+" minuti",e+" minutit"],h:["\xfche tunni","tund aega","\xfcks tund"],hh:[e+" tunni",e+" tundi"],d:["\xfche p\xe4eva","\xfcks p\xe4ev"],M:["kuu aja","kuu aega","\xfcks kuu"],MM:[e+" kuu",e+" kuud"],y:["\xfche aasta","aasta","\xfcks aasta"],yy:[e+" aasta",e+" aastat"]};return a?n[t][2]?n[t][2]:n[t][1]:s?n[t][0]:n[t][1]}function _a(e,a,t,s){var n="";switch(t){case"s":return s?"muutaman sekunnin":"muutama sekunti";case"ss":return s?"sekunnin":"sekuntia";case"m":return s?"minuutin":"minuutti";case"mm":n=s?"minuutin":"minuuttia";break;case"h":return s?"tunnin":"tunti";case"hh":n=s?"tunnin":"tuntia";break;case"d":return s?"p\xe4iv\xe4n":"p\xe4iv\xe4";case"dd":n=s?"p\xe4iv\xe4n":"p\xe4iv\xe4\xe4";break;case"M":return s?"kuukauden":"kuukausi";case"MM":n=s?"kuukauden":"kuukautta";break;case"y":return s?"vuoden":"vuosi";case"yy":n=s?"vuoden":"vuotta";break}return n=function(e,a){return e<10?a?mn[e]:on[e]:e}(e,s)+" "+n}function ia(e,a,t,s){var n={s:["thodde secondanim","thodde second"],ss:[e+" secondanim",e+" second"],m:["eka mintan","ek minute"],mm:[e+" mintanim",e+" mintam"],h:["eka horan","ek hor"],hh:[e+" horanim",e+" hor"],d:["eka disan","ek dis"],dd:[e+" disanim",e+" dis"],M:["eka mhoinean","ek mhoino"],MM:[e+" mhoineanim",e+" mhoine"],y:["eka vorsan","ek voros"],yy:[e+" vorsanim",e+" vorsam"]};return a?n[t][0]:n[t][1]}function oa(e,a,t){var s=e+" ";switch(t){case"ss":return s+=1===e?"sekunda":2===e||3===e||4===e?"sekunde":"sekundi";case"m":return a?"jedna minuta":"jedne minute";case"mm":return s+=1===e?"minuta":2===e||3===e||4===e?"minute":"minuta";case"h":return a?"jedan sat":"jednog sata";case"hh":return s+=1===e?"sat":2===e||3===e||4===e?"sata":"sati";case"dd":return s+=1===e?"dan":"dana";case"MM":return s+=1===e?"mjesec":2===e||3===e||4===e?"mjeseca":"mjeseci";case"yy":return s+=1===e?"godina":2===e||3===e||4===e?"godine":"godina"}}function ma(e,a,t,s){var n=e;switch(t){case"s":return s||a?"n\xe9h\xe1ny m\xe1sodperc":"n\xe9h\xe1ny m\xe1sodperce";case"ss":return n+(s||a)?" m\xe1sodperc":" m\xe1sodperce";case"m":return"egy"+(s||a?" perc":" perce");case"mm":return n+(s||a?" perc":" perce");case"h":return"egy"+(s||a?" \xf3ra":" \xf3r\xe1ja");case"hh":return n+(s||a?" \xf3ra":" \xf3r\xe1ja");case"d":return"egy"+(s||a?" nap":" napja");case"dd":return n+(s||a?" nap":" napja");case"M":return"egy"+(s||a?" h\xf3nap":" h\xf3napja");case"MM":return n+(s||a?" h\xf3nap":" h\xf3napja");case"y":return"egy"+(s||a?" \xe9v":" \xe9ve");case"yy":return n+(s||a?" \xe9v":" \xe9ve")}return""}function ua(e){return(e?"":"[m\xfalt] ")+"["+Yn[this.day()]+"] LT[-kor]"}function la(e){return e%100==11||e%10!=1}function Ma(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"nokkrar sek\xfandur":"nokkrum sek\xfandum";case"ss":return la(e)?n+(a||s?"sek\xfandur":"sek\xfandum"):n+"sek\xfanda";case"m":return a?"m\xedn\xfata":"m\xedn\xfatu";case"mm":return la(e)?n+(a||s?"m\xedn\xfatur":"m\xedn\xfatum"):a?n+"m\xedn\xfata":n+"m\xedn\xfatu";case"hh":return la(e)?n+(a||s?"klukkustundir":"klukkustundum"):n+"klukkustund";case"d":return a?"dagur":s?"dag":"degi";case"dd":return la(e)?a?n+"dagar":n+(s?"daga":"d\xf6gum"):a?n+"dagur":n+(s?"dag":"degi");case"M":return a?"m\xe1nu\xf0ur":s?"m\xe1nu\xf0":"m\xe1nu\xf0i";case"MM":return la(e)?a?n+"m\xe1nu\xf0ir":n+(s?"m\xe1nu\xf0i":"m\xe1nu\xf0um"):a?n+"m\xe1nu\xf0ur":n+(s?"m\xe1nu\xf0":"m\xe1nu\xf0i");case"y":return a||s?"\xe1r":"\xe1ri";case"yy":return la(e)?n+(a||s?"\xe1r":"\xe1rum"):n+(a||s?"\xe1r":"\xe1ri")}}function ha(e,a,t,s){var n={m:["eng Minutt","enger Minutt"],h:["eng Stonn","enger Stonn"],d:["een Dag","engem Dag"],M:["ee Mount","engem Mount"],y:["ee Joer","engem Joer"]};return a?n[t][0]:n[t][1]}function La(e){if(e=parseInt(e,10),isNaN(e))return!1;if(e<0)return!0;if(e<10)return 4<=e&&e<=7;if(e<100){var a=e%10;return La(0===a?e/10:a)}if(e<1e4){for(;e>=10;)e/=10;return La(e)}return e/=1e3,La(e)}function ca(e,a,t,s){return a?ya(t)[0]:s?ya(t)[1]:ya(t)[2]}function Ya(e){return e%10==0||e>10&&e<20}function ya(e){return Dn[e].split("_")}function fa(e,a,t,s){var n=e+" ";return 1===e?n+ca(0,a,t[0],s):a?n+(Ya(e)?ya(t)[1]:ya(t)[0]):s?n+ya(t)[1]:n+(Ya(e)?ya(t)[1]:ya(t)[2])}function ka(e,a,t){return t?a%10==1&&a%100!=11?e[2]:e[3]:a%10==1&&a%100!=11?e[0]:e[1]}function pa(e,a,t){return e+" "+ka(Tn[t],e,a)}function Da(e,a,t){return ka(Tn[t],e,a)}function Ta(e,a,t,s){var n="";if(a)switch(t){case"s":n="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926";break;case"ss":n="%d \u0938\u0947\u0915\u0902\u0926";break;case"m":n="\u090f\u0915 \u092e\u093f\u0928\u093f\u091f";break;case"mm":n="%d \u092e\u093f\u0928\u093f\u091f\u0947";break;case"h":n="\u090f\u0915 \u0924\u093e\u0938";break;case"hh":n="%d \u0924\u093e\u0938";break;case"d":n="\u090f\u0915 \u0926\u093f\u0935\u0938";break;case"dd":n="%d \u0926\u093f\u0935\u0938";break;case"M":n="\u090f\u0915 \u092e\u0939\u093f\u0928\u093e";break;case"MM":n="%d \u092e\u0939\u093f\u0928\u0947";break;case"y":n="\u090f\u0915 \u0935\u0930\u094d\u0937";break;case"yy":n="%d \u0935\u0930\u094d\u0937\u0947";break}else switch(t){case"s":n="\u0915\u093e\u0939\u0940 \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"ss":n="%d \u0938\u0947\u0915\u0902\u0926\u093e\u0902";break;case"m":n="\u090f\u0915\u093e \u092e\u093f\u0928\u093f\u091f\u093e";break;case"mm":n="%d \u092e\u093f\u0928\u093f\u091f\u093e\u0902";break;case"h":n="\u090f\u0915\u093e \u0924\u093e\u0938\u093e";break;case"hh":n="%d \u0924\u093e\u0938\u093e\u0902";break;case"d":n="\u090f\u0915\u093e \u0926\u093f\u0935\u0938\u093e";break;case"dd":n="%d \u0926\u093f\u0935\u0938\u093e\u0902";break;case"M":n="\u090f\u0915\u093e \u092e\u0939\u093f\u0928\u094d\u092f\u093e";break;case"MM":n="%d \u092e\u0939\u093f\u0928\u094d\u092f\u093e\u0902";break;case"y":n="\u090f\u0915\u093e \u0935\u0930\u094d\u0937\u093e";break;case"yy":n="%d \u0935\u0930\u094d\u0937\u093e\u0902";break}return n.replace(/%d/i,e)}function ga(e){return e%10<5&&e%10>1&&~~(e/10)%10!=1}function wa(e,a,t){var s=e+" ";switch(t){case"ss":return s+(ga(e)?"sekundy":"sekund");case"m":return a?"minuta":"minut\u0119";case"mm":return s+(ga(e)?"minuty":"minut");case"h":return a?"godzina":"godzin\u0119";case"hh":return s+(ga(e)?"godziny":"godzin");case"MM":return s+(ga(e)?"miesi\u0105ce":"miesi\u0119cy");case"yy":return s+(ga(e)?"lata":"lat")}}function va(e,a,t){var s=" ";return(e%100>=20||e>=100&&e%100==0)&&(s=" de "),e+s+{ss:"secunde",mm:"minute",hh:"ore",dd:"zile",MM:"luni",yy:"ani"}[t]}function Sa(e,a,t){return"m"===t?a?"\u043c\u0438\u043d\u0443\u0442\u0430":"\u043c\u0438\u043d\u0443\u0442\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u044b_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u043c\u0438\u043d\u0443\u0442\u0430_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442":"\u043c\u0438\u043d\u0443\u0442\u0443_\u043c\u0438\u043d\u0443\u0442\u044b_\u043c\u0438\u043d\u0443\u0442",hh:"\u0447\u0430\u0441_\u0447\u0430\u0441\u0430_\u0447\u0430\u0441\u043e\u0432",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u044f_\u0434\u043d\u0435\u0439",MM:"\u043c\u0435\u0441\u044f\u0446_\u043c\u0435\u0441\u044f\u0446\u0430_\u043c\u0435\u0441\u044f\u0446\u0435\u0432",yy:"\u0433\u043e\u0434_\u0433\u043e\u0434\u0430_\u043b\u0435\u0442"}[t],+e)}function Ha(e){return e>1&&e<5}function ba(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"p\xe1r sek\xfand":"p\xe1r sekundami";case"ss":return a||s?n+(Ha(e)?"sekundy":"sek\xfand"):n+"sekundami";break;case"m":return a?"min\xfata":s?"min\xfatu":"min\xfatou";case"mm":return a||s?n+(Ha(e)?"min\xfaty":"min\xfat"):n+"min\xfatami";break;case"h":return a?"hodina":s?"hodinu":"hodinou";case"hh":return a||s?n+(Ha(e)?"hodiny":"hod\xedn"):n+"hodinami";break;case"d":return a||s?"de\u0148":"d\u0148om";case"dd":return a||s?n+(Ha(e)?"dni":"dn\xed"):n+"d\u0148ami";break;case"M":return a||s?"mesiac":"mesiacom";case"MM":return a||s?n+(Ha(e)?"mesiace":"mesiacov"):n+"mesiacmi";break;case"y":return a||s?"rok":"rokom";case"yy":return a||s?n+(Ha(e)?"roky":"rokov"):n+"rokmi";break}}function ja(e,a,t,s){var n=e+" ";switch(t){case"s":return a||s?"nekaj sekund":"nekaj sekundami";case"ss":return n+=1===e?a?"sekundo":"sekundi":2===e?a||s?"sekundi":"sekundah":e<5?a||s?"sekunde":"sekundah":"sekund";case"m":return a?"ena minuta":"eno minuto";case"mm":return n+=1===e?a?"minuta":"minuto":2===e?a||s?"minuti":"minutama":e<5?a||s?"minute":"minutami":a||s?"minut":"minutami";case"h":return a?"ena ura":"eno uro";case"hh":return n+=1===e?a?"ura":"uro":2===e?a||s?"uri":"urama":e<5?a||s?"ure":"urami":a||s?"ur":"urami";case"d":return a||s?"en dan":"enim dnem";case"dd":return n+=1===e?a||s?"dan":"dnem":2===e?a||s?"dni":"dnevoma":a||s?"dni":"dnevi";case"M":return a||s?"en mesec":"enim mesecem";case"MM":return n+=1===e?a||s?"mesec":"mesecem":2===e?a||s?"meseca":"mesecema":e<5?a||s?"mesece":"meseci":a||s?"mesecev":"meseci";case"y":return a||s?"eno leto":"enim letom";case"yy":return n+=1===e?a||s?"leto":"letom":2===e?a||s?"leti":"letoma":e<5?a||s?"leta":"leti":a||s?"let":"leti"}}function xa(e,a,t,s){var n=function(e){var a=Math.floor(e%1e3/100),t=Math.floor(e%100/10),s=e%10,n="";a>0&&(n+=Qn[a]+"vatlh");t>0&&(n+=(""!==n?" ":"")+Qn[t]+"maH");s>0&&(n+=(""!==n?" ":"")+Qn[s]);return""===n?"pagh":n}(e);switch(t){case"ss":return n+" lup";case"mm":return n+" tup";case"hh":return n+" rep";case"dd":return n+" jaj";case"MM":return n+" jar";case"yy":return n+" DIS"}}function Pa(e,a,t,s){var n={s:["viensas secunds","'iensas secunds"],ss:[e+" secunds",e+" secunds"],m:["'n m\xedut","'iens m\xedut"],mm:[e+" m\xeduts",e+" m\xeduts"],h:["'n \xfeora","'iensa \xfeora"],hh:[e+" \xfeoras",e+" \xfeoras"],d:["'n ziua","'iensa ziua"],dd:[e+" ziuas",e+" ziuas"],M:["'n mes","'iens mes"],MM:[e+" mesen",e+" mesen"],y:["'n ar","'iens ar"],yy:[e+" ars",e+" ars"]};return s?n[t][0]:a?n[t][0]:n[t][1]}function Oa(e,a,t){return"m"===t?a?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443":"h"===t?a?"\u0433\u043e\u0434\u0438\u043d\u0430":"\u0433\u043e\u0434\u0438\u043d\u0443":e+" "+function(e,a){var t=e.split("_");return a%10==1&&a%100!=11?t[0]:a%10>=2&&a%10<=4&&(a%100<10||a%100>=20)?t[1]:t[2]}({ss:a?"\u0441\u0435\u043a\u0443\u043d\u0434\u0430_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434":"\u0441\u0435\u043a\u0443\u043d\u0434\u0443_\u0441\u0435\u043a\u0443\u043d\u0434\u0438_\u0441\u0435\u043a\u0443\u043d\u0434",mm:a?"\u0445\u0432\u0438\u043b\u0438\u043d\u0430_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d":"\u0445\u0432\u0438\u043b\u0438\u043d\u0443_\u0445\u0432\u0438\u043b\u0438\u043d\u0438_\u0445\u0432\u0438\u043b\u0438\u043d",hh:a?"\u0433\u043e\u0434\u0438\u043d\u0430_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d":"\u0433\u043e\u0434\u0438\u043d\u0443_\u0433\u043e\u0434\u0438\u043d\u0438_\u0433\u043e\u0434\u0438\u043d",dd:"\u0434\u0435\u043d\u044c_\u0434\u043d\u0456_\u0434\u043d\u0456\u0432",MM:"\u043c\u0456\u0441\u044f\u0446\u044c_\u043c\u0456\u0441\u044f\u0446\u0456_\u043c\u0456\u0441\u044f\u0446\u0456\u0432",yy:"\u0440\u0456\u043a_\u0440\u043e\u043a\u0438_\u0440\u043e\u043a\u0456\u0432"}[t],+e)}function Wa(e){return function(){return e+"\u043e"+(11===this.hours()?"\u0431":"")+"] LT"}}var Ea,Aa;Aa=Array.prototype.some?Array.prototype.some:function(e){for(var a=Object(this),t=a.length>>>0,s=0;s68?1900:2e3)};var kt,pt=I("FullYear",!0);kt=Array.prototype.indexOf?Array.prototype.indexOf:function(e){var a;for(a=0;athis?this:e:l()}),Zt=["year","quarter","month","week","day","hour","minute","second","millisecond"];Te("Z",":"),Te("ZZ",""),W("Z",_t),W("ZZ",_t),F(["Z","ZZ"],function(e,a,t){t._useUTC=!0,t._tzm=ge(_t,e)});var $t=/([\+\-]|\d\d)/gi;e.updateOffset=function(){};var Bt=/^(\-|\+)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,qt=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;He.fn=ke.prototype,He.invalid=function(){return He(NaN)};var Qt=xe(1,"add"),Xt=xe(-1,"subtract");e.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",e.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var es=k("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(e){return void 0===e?this.localeData():this.locale(e)});j(0,["gg",2],0,function(){return this.weekYear()%100}),j(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Ae("gggg","weekYear"),Ae("ggggg","weekYear"),Ae("GGGG","isoWeekYear"),Ae("GGGGG","isoWeekYear"),w("weekYear","gg"),w("isoWeekYear","GG"),H("weekYear",1),H("isoWeekYear",1),W("G",dt),W("g",dt),W("GG",Qa,Za),W("gg",Qa,Za),W("GGGG",tt,Ba),W("gggg",tt,Ba),W("GGGGG",st,qa),W("ggggg",st,qa),z(["gggg","ggggg","GGGG","GGGGG"],function(e,a,t,s){a[s.substr(0,2)]=Y(e)}),z(["gg","GG"],function(a,t,s,n){t[n]=e.parseTwoDigitYear(a)}),j("Q",0,"Qo","quarter"),w("quarter","Q"),H("quarter",7),W("Q",Ka),F("Q",function(e,a){a[lt]=3*(Y(e)-1)}),j("D",["DD",2],"Do","date"),w("date","D"),H("date",9),W("D",Qa),W("DD",Qa,Za),W("Do",function(e,a){return e?a._dayOfMonthOrdinalParse||a._ordinalParse:a._dayOfMonthOrdinalParseLenient}),F(["D","DD"],Mt),F("Do",function(e,a){a[Mt]=Y(e.match(Qa)[0])});var as=I("Date",!0);j("DDD",["DDDD",3],"DDDo","dayOfYear"),w("dayOfYear","DDD"),H("dayOfYear",4),W("DDD",at),W("DDDD",$a),F(["DDD","DDDD"],function(e,a,t){t._dayOfYear=Y(e)}),j("m",["mm",2],0,"minute"),w("minute","m"),H("minute",14),W("m",Qa),W("mm",Qa,Za),F(["m","mm"],Lt);var ts=I("Minutes",!1);j("s",["ss",2],0,"second"),w("second","s"),H("second",15),W("s",Qa),W("ss",Qa,Za),F(["s","ss"],ct);var ss=I("Seconds",!1);j("S",0,0,function(){return~~(this.millisecond()/100)}),j(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),j(0,["SSS",3],0,"millisecond"),j(0,["SSSS",4],0,function(){return 10*this.millisecond()}),j(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),j(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),j(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),j(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),j(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),w("millisecond","ms"),H("millisecond",16),W("S",at,Ka),W("SS",at,Za),W("SSS",at,$a);var ns;for(ns="SSSS";ns.length<=9;ns+="S")W(ns,nt);for(ns="S";ns.length<=9;ns+="S")F(ns,ze);var ds=I("Milliseconds",!1);j("z",0,0,"zoneAbbr"),j("zz",0,0,"zoneName");var rs=h.prototype;rs.add=Qt,rs.calendar=function(a,t){var s=a||ye(),n=we(s,this).startOf("day"),d=e.calendarFormat(this,n)||"sameElse",r=t&&(D(t[d])?t[d].call(this,s):t[d]);return this.format(r||this.localeData().calendar(d,this,ye(s)))},rs.clone=function(){return new h(this)},rs.diff=function(e,a,t){var s,n,d;if(!this.isValid())return NaN;if(!(s=we(e,this)).isValid())return NaN;switch(n=6e4*(s.utcOffset()-this.utcOffset()),a=v(a)){case"year":d=Oe(this,s)/12;break;case"month":d=Oe(this,s);break;case"quarter":d=Oe(this,s)/3;break;case"second":d=(this-s)/1e3;break;case"minute":d=(this-s)/6e4;break;case"hour":d=(this-s)/36e5;break;case"day":d=(this-s-n)/864e5;break;case"week":d=(this-s-n)/6048e5;break;default:d=this-s}return t?d:c(d)},rs.endOf=function(e){return void 0===(e=v(e))||"millisecond"===e?this:("date"===e&&(e="day"),this.startOf(e).add(1,"isoWeek"===e?"week":e).subtract(1,"ms"))},rs.format=function(a){a||(a=this.isUtc()?e.defaultFormatUtc:e.defaultFormat);var t=P(this,a);return this.localeData().postformat(t)},rs.from=function(e,a){return this.isValid()&&(L(e)&&e.isValid()||ye(e).isValid())?He({to:this,from:e}).locale(this.locale()).humanize(!a):this.localeData().invalidDate()},rs.fromNow=function(e){return this.from(ye(),e)},rs.to=function(e,a){return this.isValid()&&(L(e)&&e.isValid()||ye(e).isValid())?He({from:this,to:e}).locale(this.locale()).humanize(!a):this.localeData().invalidDate()},rs.toNow=function(e){return this.to(ye(),e)},rs.get=function(e){return e=v(e),D(this[e])?this[e]():this},rs.invalidAt=function(){return m(this).overflow},rs.isAfter=function(e,a){var t=L(e)?e:ye(e);return!(!this.isValid()||!t.isValid())&&("millisecond"===(a=v(s(a)?"millisecond":a))?this.valueOf()>t.valueOf():t.valueOf()9999?P(t,a?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):D(Date.prototype.toISOString)?a?this.toDate().toISOString():new Date(this._d.valueOf()).toISOString().replace("Z",P(t,"Z")):P(t,a?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")},rs.inspect=function(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e="moment",a="";this.isLocal()||(e=0===this.utcOffset()?"moment.utc":"moment.parseZone",a="Z");var t="["+e+'("]',s=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",n=a+'[")]';return this.format(t+s+"-MM-DD[T]HH:mm:ss.SSS"+n)},rs.toJSON=function(){return this.isValid()?this.toISOString():null},rs.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},rs.unix=function(){return Math.floor(this.valueOf()/1e3)},rs.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},rs.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},rs.year=pt,rs.isLeapYear=function(){return R(this.year())},rs.weekYear=function(e){return Fe.call(this,e,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},rs.isoWeekYear=function(e){return Fe.call(this,e,this.isoWeek(),this.isoWeekday(),1,4)},rs.quarter=rs.quarters=function(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)},rs.month=K,rs.daysInMonth=function(){return U(this.year(),this.month())},rs.week=rs.weeks=function(e){var a=this.localeData().week(this);return null==e?a:this.add(7*(e-a),"d")},rs.isoWeek=rs.isoWeeks=function(e){var a=Q(this,1,4).week;return null==e?a:this.add(7*(e-a),"d")},rs.weeksInYear=function(){var e=this.localeData()._week;return X(this.year(),e.dow,e.doy)},rs.isoWeeksInYear=function(){return X(this.year(),1,4)},rs.date=as,rs.day=rs.days=function(e){if(!this.isValid())return null!=e?this:NaN;var a=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=function(e,a){return"string"!=typeof e?e:isNaN(e)?"number"==typeof(e=a.weekdaysParse(e))?e:null:parseInt(e,10)}(e,this.localeData()),this.add(e-a,"d")):a},rs.weekday=function(e){if(!this.isValid())return null!=e?this:NaN;var a=(this.day()+7-this.localeData()._week.dow)%7;return null==e?a:this.add(e-a,"d")},rs.isoWeekday=function(e){if(!this.isValid())return null!=e?this:NaN;if(null!=e){var a=function(e,a){return"string"==typeof e?a.weekdaysParse(e)%7||7:isNaN(e)?null:e}(e,this.localeData());return this.day(this.day()%7?a:a-7)}return this.day()||7},rs.dayOfYear=function(e){var a=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?a:this.add(e-a,"d")},rs.hour=rs.hours=Wt,rs.minute=rs.minutes=ts,rs.second=rs.seconds=ss,rs.millisecond=rs.milliseconds=ds,rs.utcOffset=function(a,t,s){var n,d=this._offset||0;if(!this.isValid())return null!=a?this:NaN;if(null!=a){if("string"==typeof a){if(null===(a=ge(_t,a)))return this}else Math.abs(a)<16&&!s&&(a*=60);return!this._isUTC&&t&&(n=ve(this)),this._offset=a,this._isUTC=!0,null!=n&&this.add(n,"m"),d!==a&&(!t||this._changeInProgress?Pe(this,He(a-d,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,e.updateOffset(this,!0),this._changeInProgress=null)),this}return this._isUTC?d:ve(this)},rs.utc=function(e){return this.utcOffset(0,e)},rs.local=function(e){return this._isUTC&&(this.utcOffset(0,e),this._isUTC=!1,e&&this.subtract(ve(this),"m")),this},rs.parseZone=function(){if(null!=this._tzm)this.utcOffset(this._tzm,!1,!0);else if("string"==typeof this._i){var e=ge(rt,this._i);null!=e?this.utcOffset(e):this.utcOffset(0,!0)}return this},rs.hasAlignedHourOffset=function(e){return!!this.isValid()&&(e=e?ye(e).utcOffset():0,(this.utcOffset()-e)%60==0)},rs.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},rs.isLocal=function(){return!!this.isValid()&&!this._isUTC},rs.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},rs.isUtc=Se,rs.isUTC=Se,rs.zoneAbbr=function(){return this._isUTC?"UTC":""},rs.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},rs.dates=k("dates accessor is deprecated. Use date instead.",as),rs.months=k("months accessor is deprecated. Use month instead",K),rs.years=k("years accessor is deprecated. Use year instead",pt),rs.zone=k("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,a){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,a),this):-this.utcOffset()}),rs.isDSTShifted=k("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!s(this._isDSTShifted))return this._isDSTShifted;var e={};if(M(e,this),(e=ce(e))._a){var a=e._isUTC?o(e._a):ye(e._a);this._isDSTShifted=this.isValid()&&y(e._a,a.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted});var _s=g.prototype;_s.calendar=function(e,a,t){var s=this._calendar[e]||this._calendar.sameElse;return D(s)?s.call(a,t):s},_s.longDateFormat=function(e){var a=this._longDateFormat[e],t=this._longDateFormat[e.toUpperCase()];return a||!t?a:(this._longDateFormat[e]=t.replace(/MMMM|MM|DD|dddd/g,function(e){return e.slice(1)}),this._longDateFormat[e])},_s.invalidDate=function(){return this._invalidDate},_s.ordinal=function(e){return this._ordinal.replace("%d",e)},_s.preparse=Je,_s.postformat=Je,_s.relativeTime=function(e,a,t,s){var n=this._relativeTime[t];return D(n)?n(e,a,t,s):n.replace(/%d/i,e)},_s.pastFuture=function(e,a){var t=this._relativeTime[e>0?"future":"past"];return D(t)?t(a):t.replace(/%s/i,a)},_s.set=function(e){var a,t;for(t in e)D(a=e[t])?this[t]=a:this["_"+t]=a;this._config=e,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},_s.months=function(e,t){return e?a(this._months)?this._months[e.month()]:this._months[(this._months.isFormat||Dt).test(t)?"format":"standalone"][e.month()]:a(this._months)?this._months:this._months.standalone},_s.monthsShort=function(e,t){return e?a(this._monthsShort)?this._monthsShort[e.month()]:this._monthsShort[Dt.test(t)?"format":"standalone"][e.month()]:a(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},_s.monthsParse=function(e,a,t){var s,n,d;if(this._monthsParseExact)return function(e,a,t){var s,n,d,r=e.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],s=0;s<12;++s)d=o([2e3,s]),this._shortMonthsParse[s]=this.monthsShort(d,"").toLocaleLowerCase(),this._longMonthsParse[s]=this.months(d,"").toLocaleLowerCase();return t?"MMM"===a?-1!==(n=kt.call(this._shortMonthsParse,r))?n:null:-1!==(n=kt.call(this._longMonthsParse,r))?n:null:"MMM"===a?-1!==(n=kt.call(this._shortMonthsParse,r))?n:-1!==(n=kt.call(this._longMonthsParse,r))?n:null:-1!==(n=kt.call(this._longMonthsParse,r))?n:-1!==(n=kt.call(this._shortMonthsParse,r))?n:null}.call(this,e,a,t);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),s=0;s<12;s++){if(n=o([2e3,s]),t&&!this._longMonthsParse[s]&&(this._longMonthsParse[s]=new RegExp("^"+this.months(n,"").replace(".","")+"$","i"),this._shortMonthsParse[s]=new RegExp("^"+this.monthsShort(n,"").replace(".","")+"$","i")),t||this._monthsParse[s]||(d="^"+this.months(n,"")+"|^"+this.monthsShort(n,""),this._monthsParse[s]=new RegExp(d.replace(".",""),"i")),t&&"MMMM"===a&&this._longMonthsParse[s].test(e))return s;if(t&&"MMM"===a&&this._shortMonthsParse[s].test(e))return s;if(!t&&this._monthsParse[s].test(e))return s}},_s.monthsRegex=function(e){return this._monthsParseExact?(_(this,"_monthsRegex")||Z.call(this),e?this._monthsStrictRegex:this._monthsRegex):(_(this,"_monthsRegex")||(this._monthsRegex=vt),this._monthsStrictRegex&&e?this._monthsStrictRegex:this._monthsRegex)},_s.monthsShortRegex=function(e){return this._monthsParseExact?(_(this,"_monthsRegex")||Z.call(this),e?this._monthsShortStrictRegex:this._monthsShortRegex):(_(this,"_monthsShortRegex")||(this._monthsShortRegex=wt),this._monthsShortStrictRegex&&e?this._monthsShortStrictRegex:this._monthsShortRegex)},_s.week=function(e){return Q(e,this._week.dow,this._week.doy).week},_s.firstDayOfYear=function(){return this._week.doy},_s.firstDayOfWeek=function(){return this._week.dow},_s.weekdays=function(e,t){return e?a(this._weekdays)?this._weekdays[e.day()]:this._weekdays[this._weekdays.isFormat.test(t)?"format":"standalone"][e.day()]:a(this._weekdays)?this._weekdays:this._weekdays.standalone},_s.weekdaysMin=function(e){return e?this._weekdaysMin[e.day()]:this._weekdaysMin},_s.weekdaysShort=function(e){return e?this._weekdaysShort[e.day()]:this._weekdaysShort},_s.weekdaysParse=function(e,a,t){var s,n,d;if(this._weekdaysParseExact)return function(e,a,t){var s,n,d,r=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],s=0;s<7;++s)d=o([2e3,1]).day(s),this._minWeekdaysParse[s]=this.weekdaysMin(d,"").toLocaleLowerCase(),this._shortWeekdaysParse[s]=this.weekdaysShort(d,"").toLocaleLowerCase(),this._weekdaysParse[s]=this.weekdays(d,"").toLocaleLowerCase();return t?"dddd"===a?-1!==(n=kt.call(this._weekdaysParse,r))?n:null:"ddd"===a?-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:null:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:"dddd"===a?-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:"ddd"===a?-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:null:-1!==(n=kt.call(this._minWeekdaysParse,r))?n:-1!==(n=kt.call(this._weekdaysParse,r))?n:-1!==(n=kt.call(this._shortWeekdaysParse,r))?n:null}.call(this,e,a,t);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),s=0;s<7;s++){if(n=o([2e3,1]).day(s),t&&!this._fullWeekdaysParse[s]&&(this._fullWeekdaysParse[s]=new RegExp("^"+this.weekdays(n,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[s]=new RegExp("^"+this.weekdaysShort(n,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[s]=new RegExp("^"+this.weekdaysMin(n,"").replace(".",".?")+"$","i")),this._weekdaysParse[s]||(d="^"+this.weekdays(n,"")+"|^"+this.weekdaysShort(n,"")+"|^"+this.weekdaysMin(n,""),this._weekdaysParse[s]=new RegExp(d.replace(".",""),"i")),t&&"dddd"===a&&this._fullWeekdaysParse[s].test(e))return s;if(t&&"ddd"===a&&this._shortWeekdaysParse[s].test(e))return s;if(t&&"dd"===a&&this._minWeekdaysParse[s].test(e))return s;if(!t&&this._weekdaysParse[s].test(e))return s}},_s.weekdaysRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(_(this,"_weekdaysRegex")||(this._weekdaysRegex=jt),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)},_s.weekdaysShortRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(_(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=xt),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},_s.weekdaysMinRegex=function(e){return this._weekdaysParseExact?(_(this,"_weekdaysRegex")||ee.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(_(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Pt),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},_s.isPM=function(e){return"p"===(e+"").toLowerCase().charAt(0)},_s.meridiem=function(e,a,t){return e>11?t?"pm":"PM":t?"am":"AM"},re("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var a=e%10;return e+(1===Y(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")}}),e.lang=k("moment.lang is deprecated. Use moment.locale instead.",re),e.langData=k("moment.langData is deprecated. Use moment.localeData instead.",ie);var is=Math.abs,os=Ke("ms"),ms=Ke("s"),us=Ke("m"),ls=Ke("h"),Ms=Ke("d"),hs=Ke("w"),Ls=Ke("M"),cs=Ke("y"),Ys=Ze("milliseconds"),ys=Ze("seconds"),fs=Ze("minutes"),ks=Ze("hours"),ps=Ze("days"),Ds=Ze("months"),Ts=Ze("years"),gs=Math.round,ws={ss:44,s:45,m:45,h:22,d:26,M:11},vs=Math.abs,Ss=ke.prototype;Ss.isValid=function(){return this._isValid},Ss.abs=function(){var e=this._data;return this._milliseconds=is(this._milliseconds),this._days=is(this._days),this._months=is(this._months),e.milliseconds=is(e.milliseconds),e.seconds=is(e.seconds),e.minutes=is(e.minutes),e.hours=is(e.hours),e.months=is(e.months),e.years=is(e.years),this},Ss.add=function(e,a){return Ce(this,e,a,1)},Ss.subtract=function(e,a){return Ce(this,e,a,-1)},Ss.as=function(e){if(!this.isValid())return NaN;var a,t,s=this._milliseconds;if("month"===(e=v(e))||"year"===e)return a=this._days+s/864e5,t=this._months+Ue(a),"month"===e?t:t/12;switch(a=this._days+Math.round(Ve(this._months)),e){case"week":return a/7+s/6048e5;case"day":return a+s/864e5;case"hour":return 24*a+s/36e5;case"minute":return 1440*a+s/6e4;case"second":return 86400*a+s/1e3;case"millisecond":return Math.floor(864e5*a)+s;default:throw new Error("Unknown unit "+e)}},Ss.asMilliseconds=os,Ss.asSeconds=ms,Ss.asMinutes=us,Ss.asHours=ls,Ss.asDays=Ms,Ss.asWeeks=hs,Ss.asMonths=Ls,Ss.asYears=cs,Ss.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*Y(this._months/12):NaN},Ss._bubble=function(){var e,a,t,s,n,d=this._milliseconds,r=this._days,_=this._months,i=this._data;return d>=0&&r>=0&&_>=0||d<=0&&r<=0&&_<=0||(d+=864e5*Ge(Ve(_)+r),r=0,_=0),i.milliseconds=d%1e3,e=c(d/1e3),i.seconds=e%60,a=c(e/60),i.minutes=a%60,t=c(a/60),i.hours=t%24,r+=c(t/24),n=c(Ue(r)),_+=n,r-=Ge(Ve(n)),s=c(_/12),_%=12,i.days=r,i.months=_,i.years=s,this},Ss.clone=function(){return He(this)},Ss.get=function(e){return e=v(e),this.isValid()?this[e+"s"]():NaN},Ss.milliseconds=Ys,Ss.seconds=ys,Ss.minutes=fs,Ss.hours=ks,Ss.days=ps,Ss.weeks=function(){return c(this.days()/7)},Ss.months=Ds,Ss.years=Ts,Ss.humanize=function(e){if(!this.isValid())return this.localeData().invalidDate();var a=this.localeData(),t=function(e,a,t){var s=He(e).abs(),n=gs(s.as("s")),d=gs(s.as("m")),r=gs(s.as("h")),_=gs(s.as("d")),i=gs(s.as("M")),o=gs(s.as("y")),m=n<=ws.ss&&["s",n]||n0,m[4]=t,function(e,a,t,s,n){return n.relativeTime(a||1,!!t,e,s)}.apply(null,m)}(this,!e,a);return e&&(t=a.pastFuture(+this,t)),a.postformat(t)},Ss.toISOString=Be,Ss.toString=Be,Ss.toJSON=Be,Ss.locale=We,Ss.localeData=Ee,Ss.toIsoString=k("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",Be),Ss.lang=es,j("X",0,0,"unix"),j("x",0,0,"valueOf"),W("x",dt),W("X",/[+-]?\d+(\.\d{1,3})?/),F("X",function(e,a,t){t._d=new Date(1e3*parseFloat(e,10))}),F("x",function(e,a,t){t._d=new Date(Y(e))}),e.version="2.20.1",function(e){Ea=e}(ye),e.fn=rs,e.min=function(){return fe("isBefore",[].slice.call(arguments,0))},e.max=function(){return fe("isAfter",[].slice.call(arguments,0))},e.now=function(){return Date.now?Date.now():+new Date},e.utc=o,e.unix=function(e){return ye(1e3*e)},e.months=function(e,a){return Re(e,a,"months")},e.isDate=d,e.locale=re,e.invalid=l,e.duration=He,e.isMoment=L,e.weekdays=function(e,a,t){return Ie(e,a,t,"weekdays")},e.parseZone=function(){return ye.apply(null,arguments).parseZone()},e.localeData=ie,e.isDuration=pe,e.monthsShort=function(e,a){return Re(e,a,"monthsShort")},e.weekdaysMin=function(e,a,t){return Ie(e,a,t,"weekdaysMin")},e.defineLocale=_e,e.updateLocale=function(e,a){if(null!=a){var t,s,n=Et;null!=(s=de(e))&&(n=s._config),(t=new g(a=T(n,a))).parentLocale=At[e],At[e]=t,re(e)}else null!=At[e]&&(null!=At[e].parentLocale?At[e]=At[e].parentLocale:null!=At[e]&&delete At[e]);return At[e]},e.locales=function(){return Na(At)},e.weekdaysShort=function(e,a,t){return Ie(e,a,t,"weekdaysShort")},e.normalizeUnits=v,e.relativeTimeRounding=function(e){return void 0===e?gs:"function"==typeof e&&(gs=e,!0)},e.relativeTimeThreshold=function(e,a){return void 0!==ws[e]&&(void 0===a?ws[e]:(ws[e]=a,"s"===e&&(ws.ss=a-1),!0))},e.calendarFormat=function(e,a){var t=e.diff(a,"days",!0);return t<-6?"sameElse":t<-1?"lastWeek":t<0?"lastDay":t<1?"sameDay":t<2?"nextDay":t<7?"nextWeek":"sameElse"},e.prototype=rs,e.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"YYYY-[W]WW",MONTH:"YYYY-MM"},e.defineLocale("af",{months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),meridiemParse:/vm|nm/i,isPM:function(e){return/^nm$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"vm":"VM":t?"nm":"NM"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Vandag om] LT",nextDay:"[M\xf4re om] LT",nextWeek:"dddd [om] LT",lastDay:"[Gister om] LT",lastWeek:"[Laas] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",ss:"%d sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}}),e.defineLocale("ar-dz",{months:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u0623\u062d_\u0625\u062b_\u062b\u0644\u0627_\u0623\u0631_\u062e\u0645_\u062c\u0645_\u0633\u0628".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:0,doy:4}}),e.defineLocale("ar-kw",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062a\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062a\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:0,doy:12}});var Hs={1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",0:"0"},bs=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},js={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},xs=function(e){return function(a,t,s,n){var d=bs(a),r=js[e][bs(a)];return 2===d&&(r=r[t?0:1]),r.replace(/%d/i,a)}},Ps=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];e.defineLocale("ar-ly",{months:Ps,monthsShort:Ps,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:xs("s"),ss:xs("s"),m:xs("m"),mm:xs("m"),h:xs("h"),hh:xs("h"),d:xs("d"),dd:xs("d"),M:xs("M"),MM:xs("M"),y:xs("y"),yy:xs("y")},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Hs[e]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}}),e.defineLocale("ar-ma",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648\u0632_\u063a\u0634\u062a_\u0634\u062a\u0646\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0646\u0628\u0631_\u062f\u062c\u0646\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062a\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0627\u062d\u062f_\u0627\u062a\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:6,doy:12}});var Os={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},Ws={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"};e.defineLocale("ar-sa",{months:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u064a\u0646\u0627\u064a\u0631_\u0641\u0628\u0631\u0627\u064a\u0631_\u0645\u0627\u0631\u0633_\u0623\u0628\u0631\u064a\u0644_\u0645\u0627\u064a\u0648_\u064a\u0648\u0646\u064a\u0648_\u064a\u0648\u0644\u064a\u0648_\u0623\u063a\u0633\u0637\u0633_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},preparse:function(e){return e.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(e){return Ws[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Os[e]}).replace(/,/g,"\u060c")},week:{dow:0,doy:6}}),e.defineLocale("ar-tn",{months:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),monthsShort:"\u062c\u0627\u0646\u0641\u064a_\u0641\u064a\u0641\u0631\u064a_\u0645\u0627\u0631\u0633_\u0623\u0641\u0631\u064a\u0644_\u0645\u0627\u064a_\u062c\u0648\u0627\u0646_\u062c\u0648\u064a\u0644\u064a\u0629_\u0623\u0648\u062a_\u0633\u0628\u062a\u0645\u0628\u0631_\u0623\u0643\u062a\u0648\u0628\u0631_\u0646\u0648\u0641\u0645\u0628\u0631_\u062f\u064a\u0633\u0645\u0628\u0631".split("_"),weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u0627 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0644\u0649 \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0641\u064a %s",past:"\u0645\u0646\u0630 %s",s:"\u062b\u0648\u0627\u0646",ss:"%d \u062b\u0627\u0646\u064a\u0629",m:"\u062f\u0642\u064a\u0642\u0629",mm:"%d \u062f\u0642\u0627\u0626\u0642",h:"\u0633\u0627\u0639\u0629",hh:"%d \u0633\u0627\u0639\u0627\u062a",d:"\u064a\u0648\u0645",dd:"%d \u0623\u064a\u0627\u0645",M:"\u0634\u0647\u0631",MM:"%d \u0623\u0634\u0647\u0631",y:"\u0633\u0646\u0629",yy:"%d \u0633\u0646\u0648\u0627\u062a"},week:{dow:1,doy:4}});var Es={1:"\u0661",2:"\u0662",3:"\u0663",4:"\u0664",5:"\u0665",6:"\u0666",7:"\u0667",8:"\u0668",9:"\u0669",0:"\u0660"},As={"\u0661":"1","\u0662":"2","\u0663":"3","\u0664":"4","\u0665":"5","\u0666":"6","\u0667":"7","\u0668":"8","\u0669":"9","\u0660":"0"},Fs=function(e){return 0===e?0:1===e?1:2===e?2:e%100>=3&&e%100<=10?3:e%100>=11?4:5},zs={s:["\u0623\u0642\u0644 \u0645\u0646 \u062b\u0627\u0646\u064a\u0629","\u062b\u0627\u0646\u064a\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062b\u0627\u0646\u064a\u062a\u0627\u0646","\u062b\u0627\u0646\u064a\u062a\u064a\u0646"],"%d \u062b\u0648\u0627\u0646","%d \u062b\u0627\u0646\u064a\u0629","%d \u062b\u0627\u0646\u064a\u0629"],m:["\u0623\u0642\u0644 \u0645\u0646 \u062f\u0642\u064a\u0642\u0629","\u062f\u0642\u064a\u0642\u0629 \u0648\u0627\u062d\u062f\u0629",["\u062f\u0642\u064a\u0642\u062a\u0627\u0646","\u062f\u0642\u064a\u0642\u062a\u064a\u0646"],"%d \u062f\u0642\u0627\u0626\u0642","%d \u062f\u0642\u064a\u0642\u0629","%d \u062f\u0642\u064a\u0642\u0629"],h:["\u0623\u0642\u0644 \u0645\u0646 \u0633\u0627\u0639\u0629","\u0633\u0627\u0639\u0629 \u0648\u0627\u062d\u062f\u0629",["\u0633\u0627\u0639\u062a\u0627\u0646","\u0633\u0627\u0639\u062a\u064a\u0646"],"%d \u0633\u0627\u0639\u0627\u062a","%d \u0633\u0627\u0639\u0629","%d \u0633\u0627\u0639\u0629"],d:["\u0623\u0642\u0644 \u0645\u0646 \u064a\u0648\u0645","\u064a\u0648\u0645 \u0648\u0627\u062d\u062f",["\u064a\u0648\u0645\u0627\u0646","\u064a\u0648\u0645\u064a\u0646"],"%d \u0623\u064a\u0627\u0645","%d \u064a\u0648\u0645\u064b\u0627","%d \u064a\u0648\u0645"],M:["\u0623\u0642\u0644 \u0645\u0646 \u0634\u0647\u0631","\u0634\u0647\u0631 \u0648\u0627\u062d\u062f",["\u0634\u0647\u0631\u0627\u0646","\u0634\u0647\u0631\u064a\u0646"],"%d \u0623\u0634\u0647\u0631","%d \u0634\u0647\u0631\u0627","%d \u0634\u0647\u0631"],y:["\u0623\u0642\u0644 \u0645\u0646 \u0639\u0627\u0645","\u0639\u0627\u0645 \u0648\u0627\u062d\u062f",["\u0639\u0627\u0645\u0627\u0646","\u0639\u0627\u0645\u064a\u0646"],"%d \u0623\u0639\u0648\u0627\u0645","%d \u0639\u0627\u0645\u064b\u0627","%d \u0639\u0627\u0645"]},Js=function(e){return function(a,t,s,n){var d=Fs(a),r=zs[e][Fs(a)];return 2===d&&(r=r[t?0:1]),r.replace(/%d/i,a)}},Ns=["\u064a\u0646\u0627\u064a\u0631","\u0641\u0628\u0631\u0627\u064a\u0631","\u0645\u0627\u0631\u0633","\u0623\u0628\u0631\u064a\u0644","\u0645\u0627\u064a\u0648","\u064a\u0648\u0646\u064a\u0648","\u064a\u0648\u0644\u064a\u0648","\u0623\u063a\u0633\u0637\u0633","\u0633\u0628\u062a\u0645\u0628\u0631","\u0623\u0643\u062a\u0648\u0628\u0631","\u0646\u0648\u0641\u0645\u0628\u0631","\u062f\u064a\u0633\u0645\u0628\u0631"];e.defineLocale("ar",{months:Ns,monthsShort:Ns,weekdays:"\u0627\u0644\u0623\u062d\u062f_\u0627\u0644\u0625\u062b\u0646\u064a\u0646_\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621_\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621_\u0627\u0644\u062e\u0645\u064a\u0633_\u0627\u0644\u062c\u0645\u0639\u0629_\u0627\u0644\u0633\u0628\u062a".split("_"),weekdaysShort:"\u0623\u062d\u062f_\u0625\u062b\u0646\u064a\u0646_\u062b\u0644\u0627\u062b\u0627\u0621_\u0623\u0631\u0628\u0639\u0627\u0621_\u062e\u0645\u064a\u0633_\u062c\u0645\u0639\u0629_\u0633\u0628\u062a".split("_"),weekdaysMin:"\u062d_\u0646_\u062b_\u0631_\u062e_\u062c_\u0633".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/\u200fM/\u200fYYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0635|\u0645/,isPM:function(e){return"\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635":"\u0645"},calendar:{sameDay:"[\u0627\u0644\u064a\u0648\u0645 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextDay:"[\u063a\u062f\u064b\u0627 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",nextWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastDay:"[\u0623\u0645\u0633 \u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",lastWeek:"dddd [\u0639\u0646\u062f \u0627\u0644\u0633\u0627\u0639\u0629] LT",sameElse:"L"},relativeTime:{future:"\u0628\u0639\u062f %s",past:"\u0645\u0646\u0630 %s",s:Js("s"),ss:Js("s"),m:Js("m"),mm:Js("m"),h:Js("h"),hh:Js("h"),d:Js("d"),dd:Js("d"),M:Js("M"),MM:Js("M"),y:Js("y"),yy:Js("y")},preparse:function(e){return e.replace(/[\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u0660]/g,function(e){return As[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return Es[e]}).replace(/,/g,"\u060c")},week:{dow:6,doy:12}});var Rs={1:"-inci",5:"-inci",8:"-inci",70:"-inci",80:"-inci",2:"-nci",7:"-nci",20:"-nci",50:"-nci",3:"-\xfcnc\xfc",4:"-\xfcnc\xfc",100:"-\xfcnc\xfc",6:"-nc\u0131",9:"-uncu",10:"-uncu",30:"-uncu",60:"-\u0131nc\u0131",90:"-\u0131nc\u0131"};e.defineLocale("az",{months:"yanvar_fevral_mart_aprel_may_iyun_iyul_avqust_sentyabr_oktyabr_noyabr_dekabr".split("_"),monthsShort:"yan_fev_mar_apr_may_iyn_iyl_avq_sen_okt_noy_dek".split("_"),weekdays:"Bazar_Bazar ert\u0259si_\xc7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131_\xc7\u0259r\u015f\u0259nb\u0259_C\xfcm\u0259 ax\u015fam\u0131_C\xfcm\u0259_\u015e\u0259nb\u0259".split("_"),weekdaysShort:"Baz_BzE_\xc7Ax_\xc7\u0259r_CAx_C\xfcm_\u015e\u0259n".split("_"),weekdaysMin:"Bz_BE_\xc7A_\xc7\u0259_CA_C\xfc_\u015e\u0259".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[sabah saat] LT",nextWeek:"[g\u0259l\u0259n h\u0259ft\u0259] dddd [saat] LT",lastDay:"[d\xfcn\u0259n] LT",lastWeek:"[ke\xe7\u0259n h\u0259ft\u0259] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \u0259vv\u0259l",s:"birne\xe7\u0259 saniyy\u0259",ss:"%d saniy\u0259",m:"bir d\u0259qiq\u0259",mm:"%d d\u0259qiq\u0259",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir ay",MM:"%d ay",y:"bir il",yy:"%d il"},meridiemParse:/gec\u0259|s\u0259h\u0259r|g\xfcnd\xfcz|ax\u015fam/,isPM:function(e){return/^(g\xfcnd\xfcz|ax\u015fam)$/.test(e)},meridiem:function(e,a,t){return e<4?"gec\u0259":e<12?"s\u0259h\u0259r":e<17?"g\xfcnd\xfcz":"ax\u015fam"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0131nc\u0131|inci|nci|\xfcnc\xfc|nc\u0131|uncu)/,ordinal:function(e){if(0===e)return e+"-\u0131nc\u0131";var a=e%10;return e+(Rs[a]||Rs[e%100-a]||Rs[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("be",{months:{format:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044f_\u043b\u044e\u0442\u0430\u0433\u0430_\u0441\u0430\u043a\u0430\u0432\u0456\u043a\u0430_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a\u0430_\u0442\u0440\u0430\u045e\u043d\u044f_\u0447\u044d\u0440\u0432\u0435\u043d\u044f_\u043b\u0456\u043f\u0435\u043d\u044f_\u0436\u043d\u0456\u045e\u043d\u044f_\u0432\u0435\u0440\u0430\u0441\u043d\u044f_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a\u0430_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434\u0430_\u0441\u043d\u0435\u0436\u043d\u044f".split("_"),standalone:"\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c_\u043b\u044e\u0442\u044b_\u0441\u0430\u043a\u0430\u0432\u0456\u043a_\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u044d\u0440\u0432\u0435\u043d\u044c_\u043b\u0456\u043f\u0435\u043d\u044c_\u0436\u043d\u0456\u0432\u0435\u043d\u044c_\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c_\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a_\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434_\u0441\u043d\u0435\u0436\u0430\u043d\u044c".split("_")},monthsShort:"\u0441\u0442\u0443\u0434_\u043b\u044e\u0442_\u0441\u0430\u043a_\u043a\u0440\u0430\u0441_\u0442\u0440\u0430\u0432_\u0447\u044d\u0440\u0432_\u043b\u0456\u043f_\u0436\u043d\u0456\u0432_\u0432\u0435\u0440_\u043a\u0430\u0441\u0442_\u043b\u0456\u0441\u0442_\u0441\u043d\u0435\u0436".split("_"),weekdays:{format:"\u043d\u044f\u0434\u0437\u0435\u043b\u044e_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0443_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0443_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),standalone:"\u043d\u044f\u0434\u0437\u0435\u043b\u044f_\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a_\u0430\u045e\u0442\u043e\u0440\u0430\u043a_\u0441\u0435\u0440\u0430\u0434\u0430_\u0447\u0430\u0446\u0432\u0435\u0440_\u043f\u044f\u0442\u043d\u0456\u0446\u0430_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),isFormat:/\[ ?[\u0412\u0432] ?(?:\u043c\u0456\u043d\u0443\u043b\u0443\u044e|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443\u044e)? ?\] ?dddd/},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0430\u0442_\u0441\u0440_\u0447\u0446_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., HH:mm",LLLL:"dddd, D MMMM YYYY \u0433., HH:mm"},calendar:{sameDay:"[\u0421\u0451\u043d\u043d\u044f \u045e] LT",nextDay:"[\u0417\u0430\u045e\u0442\u0440\u0430 \u045e] LT",lastDay:"[\u0423\u0447\u043e\u0440\u0430 \u045e] LT",nextWeek:function(){return"[\u0423] dddd [\u045e] LT"},lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u0443\u044e] dddd [\u045e] LT";case 1:case 2:case 4:return"[\u0423 \u043c\u0456\u043d\u0443\u043b\u044b] dddd [\u045e] LT"}},sameElse:"L"},relativeTime:{future:"\u043f\u0440\u0430\u0437 %s",past:"%s \u0442\u0430\u043c\u0443",s:"\u043d\u0435\u043a\u0430\u043b\u044c\u043a\u0456 \u0441\u0435\u043a\u0443\u043d\u0434",m:qe,mm:qe,h:qe,hh:qe,d:"\u0434\u0437\u0435\u043d\u044c",dd:qe,M:"\u043c\u0435\u0441\u044f\u0446",MM:qe,y:"\u0433\u043e\u0434",yy:qe},meridiemParse:/\u043d\u043e\u0447\u044b|\u0440\u0430\u043d\u0456\u0446\u044b|\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430/,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0430\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u044b":e<12?"\u0440\u0430\u043d\u0456\u0446\u044b":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0430\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0456|\u044b|\u0433\u0430)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":case"w":case"W":return e%10!=2&&e%10!=3||e%100==12||e%100==13?e+"-\u044b":e+"-\u0456";case"D":return e+"-\u0433\u0430";default:return e}},week:{dow:1,doy:7}}),e.defineLocale("bg",{months:"\u044f\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u044f\u043d\u0440_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u044e\u043d\u0438_\u044e\u043b\u0438_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u044f\u0434\u0430_\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a_\u043f\u0435\u0442\u044a\u043a_\u0441\u044a\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u044f_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u044a\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u043d\u0435\u0441 \u0432] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432] LT",nextWeek:"dddd [\u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u0412 \u0438\u0437\u043c\u0438\u043d\u0430\u043b\u0430\u0442\u0430] dddd [\u0432] LT";case 1:case 2:case 4:case 5:return"[\u0412 \u0438\u0437\u043c\u0438\u043d\u0430\u043b\u0438\u044f] dddd [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0441\u043b\u0435\u0434 %s",past:"\u043f\u0440\u0435\u0434\u0438 %s",s:"\u043d\u044f\u043a\u043e\u043b\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0434\u0435\u043d",dd:"%d \u0434\u043d\u0438",M:"\u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0430",y:"\u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-\u0435\u0432":0===t?e+"-\u0435\u043d":t>10&&t<20?e+"-\u0442\u0438":1===a?e+"-\u0432\u0438":2===a?e+"-\u0440\u0438":7===a||8===a?e+"-\u043c\u0438":e+"-\u0442\u0438"},week:{dow:1,doy:7}}),e.defineLocale("bm",{months:"Zanwuyekalo_Fewuruyekalo_Marisikalo_Awirilikalo_M\u025bkalo_Zuw\u025bnkalo_Zuluyekalo_Utikalo_S\u025btanburukalo_\u0254kut\u0254burukalo_Nowanburukalo_Desanburukalo".split("_"),monthsShort:"Zan_Few_Mar_Awi_M\u025b_Zuw_Zul_Uti_S\u025bt_\u0254ku_Now_Des".split("_"),weekdays:"Kari_Nt\u025bn\u025bn_Tarata_Araba_Alamisa_Juma_Sibiri".split("_"),weekdaysShort:"Kar_Nt\u025b_Tar_Ara_Ala_Jum_Sib".split("_"),weekdaysMin:"Ka_Nt_Ta_Ar_Al_Ju_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"MMMM [tile] D [san] YYYY",LLL:"MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm",LLLL:"dddd MMMM [tile] D [san] YYYY [l\u025br\u025b] HH:mm"},calendar:{sameDay:"[Bi l\u025br\u025b] LT",nextDay:"[Sini l\u025br\u025b] LT",nextWeek:"dddd [don l\u025br\u025b] LT",lastDay:"[Kunu l\u025br\u025b] LT",lastWeek:"dddd [t\u025bm\u025bnen l\u025br\u025b] LT",sameElse:"L"},relativeTime:{future:"%s k\u0254n\u0254",past:"a b\u025b %s b\u0254",s:"sanga dama dama",ss:"sekondi %d",m:"miniti kelen",mm:"miniti %d",h:"l\u025br\u025b kelen",hh:"l\u025br\u025b %d",d:"tile kelen",dd:"tile %d",M:"kalo kelen",MM:"kalo %d",y:"san kelen",yy:"san %d"},week:{dow:1,doy:4}});var Is={1:"\u09e7",2:"\u09e8",3:"\u09e9",4:"\u09ea",5:"\u09eb",6:"\u09ec",7:"\u09ed",8:"\u09ee",9:"\u09ef",0:"\u09e6"},Cs={"\u09e7":"1","\u09e8":"2","\u09e9":"3","\u09ea":"4","\u09eb":"5","\u09ec":"6","\u09ed":"7","\u09ee":"8","\u09ef":"9","\u09e6":"0"};e.defineLocale("bn",{months:"\u099c\u09be\u09a8\u09c1\u09df\u09be\u09b0\u09c0_\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09df\u09be\u09b0\u09bf_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0\u09bf\u09b2_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2\u09be\u0987_\u0986\u0997\u09b8\u09cd\u099f_\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0_\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0_\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0_\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0".split("_"),monthsShort:"\u099c\u09be\u09a8\u09c1_\u09ab\u09c7\u09ac_\u09ae\u09be\u09b0\u09cd\u099a_\u098f\u09aa\u09cd\u09b0_\u09ae\u09c7_\u099c\u09c1\u09a8_\u099c\u09c1\u09b2_\u0986\u0997_\u09b8\u09c7\u09aa\u09cd\u099f_\u0985\u0995\u09cd\u099f\u09cb_\u09a8\u09ad\u09c7_\u09a1\u09bf\u09b8\u09c7".split("_"),weekdays:"\u09b0\u09ac\u09bf\u09ac\u09be\u09b0_\u09b8\u09cb\u09ae\u09ac\u09be\u09b0_\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0_\u09ac\u09c1\u09a7\u09ac\u09be\u09b0_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0_\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0_\u09b6\u09a8\u09bf\u09ac\u09be\u09b0".split("_"),weekdaysShort:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997\u09b2_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),weekdaysMin:"\u09b0\u09ac\u09bf_\u09b8\u09cb\u09ae_\u09ae\u0999\u09cd\u0997_\u09ac\u09c1\u09a7_\u09ac\u09c3\u09b9\u0983_\u09b6\u09c1\u0995\u09cd\u09b0_\u09b6\u09a8\u09bf".split("_"),longDateFormat:{LT:"A h:mm \u09b8\u09ae\u09df",LTS:"A h:mm:ss \u09b8\u09ae\u09df",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u09b8\u09ae\u09df",LLLL:"dddd, D MMMM YYYY, A h:mm \u09b8\u09ae\u09df"},calendar:{sameDay:"[\u0986\u099c] LT",nextDay:"[\u0986\u0997\u09be\u09ae\u09c0\u0995\u09be\u09b2] LT",nextWeek:"dddd, LT",lastDay:"[\u0997\u09a4\u0995\u09be\u09b2] LT",lastWeek:"[\u0997\u09a4] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u09aa\u09b0\u09c7",past:"%s \u0986\u0997\u09c7",s:"\u0995\u09df\u09c7\u0995 \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",ss:"%d \u09b8\u09c7\u0995\u09c7\u09a8\u09cd\u09a1",m:"\u098f\u0995 \u09ae\u09bf\u09a8\u09bf\u099f",mm:"%d \u09ae\u09bf\u09a8\u09bf\u099f",h:"\u098f\u0995 \u0998\u09a8\u09cd\u099f\u09be",hh:"%d \u0998\u09a8\u09cd\u099f\u09be",d:"\u098f\u0995 \u09a6\u09bf\u09a8",dd:"%d \u09a6\u09bf\u09a8",M:"\u098f\u0995 \u09ae\u09be\u09b8",MM:"%d \u09ae\u09be\u09b8",y:"\u098f\u0995 \u09ac\u099b\u09b0",yy:"%d \u09ac\u099b\u09b0"},preparse:function(e){return e.replace(/[\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u09e6]/g,function(e){return Cs[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Is[e]})},meridiemParse:/\u09b0\u09be\u09a4|\u09b8\u0995\u09be\u09b2|\u09a6\u09c1\u09aa\u09c1\u09b0|\u09ac\u09bf\u0995\u09be\u09b2|\u09b0\u09be\u09a4/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u09b0\u09be\u09a4"===a&&e>=4||"\u09a6\u09c1\u09aa\u09c1\u09b0"===a&&e<5||"\u09ac\u09bf\u0995\u09be\u09b2"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u09b0\u09be\u09a4":e<10?"\u09b8\u0995\u09be\u09b2":e<17?"\u09a6\u09c1\u09aa\u09c1\u09b0":e<20?"\u09ac\u09bf\u0995\u09be\u09b2":"\u09b0\u09be\u09a4"},week:{dow:0,doy:6}});var Gs={1:"\u0f21",2:"\u0f22",3:"\u0f23",4:"\u0f24",5:"\u0f25",6:"\u0f26",7:"\u0f27",8:"\u0f28",9:"\u0f29",0:"\u0f20"},Us={"\u0f21":"1","\u0f22":"2","\u0f23":"3","\u0f24":"4","\u0f25":"5","\u0f26":"6","\u0f27":"7","\u0f28":"8","\u0f29":"9","\u0f20":"0"};e.defineLocale("bo",{months:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54".split("_"),monthsShort:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54".split("_"),weekdays:"\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f42\u0f5f\u0f60\u0f0b\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysShort:"\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),weekdaysMin:"\u0f49\u0f72\u0f0b\u0f58\u0f0b_\u0f5f\u0fb3\u0f0b\u0f56\u0f0b_\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b_\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b_\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74_\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b_\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0f51\u0f72\u0f0b\u0f62\u0f72\u0f44] LT",nextDay:"[\u0f66\u0f44\u0f0b\u0f49\u0f72\u0f53] LT",nextWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f62\u0f97\u0f7a\u0f66\u0f0b\u0f58], LT",lastDay:"[\u0f41\u0f0b\u0f66\u0f44] LT",lastWeek:"[\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f55\u0fb2\u0f42\u0f0b\u0f58\u0f50\u0f60\u0f0b\u0f58] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0f63\u0f0b",past:"%s \u0f66\u0f94\u0f53\u0f0b\u0f63",s:"\u0f63\u0f58\u0f0b\u0f66\u0f44",ss:"%d \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f0d",m:"\u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b\u0f42\u0f45\u0f72\u0f42",mm:"%d \u0f66\u0f90\u0f62\u0f0b\u0f58",h:"\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b\u0f42\u0f45\u0f72\u0f42",hh:"%d \u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51",d:"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f45\u0f72\u0f42",dd:"%d \u0f49\u0f72\u0f53\u0f0b",M:"\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f45\u0f72\u0f42",MM:"%d \u0f5f\u0fb3\u0f0b\u0f56",y:"\u0f63\u0f7c\u0f0b\u0f42\u0f45\u0f72\u0f42",yy:"%d \u0f63\u0f7c"},preparse:function(e){return e.replace(/[\u0f21\u0f22\u0f23\u0f24\u0f25\u0f26\u0f27\u0f28\u0f29\u0f20]/g,function(e){return Us[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Gs[e]})},meridiemParse:/\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c|\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66|\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44|\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42|\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"===a&&e>=4||"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44"===a&&e<5||"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c":e<10?"\u0f5e\u0f7c\u0f42\u0f66\u0f0b\u0f40\u0f66":e<17?"\u0f49\u0f72\u0f53\u0f0b\u0f42\u0f74\u0f44":e<20?"\u0f51\u0f42\u0f7c\u0f44\u0f0b\u0f51\u0f42":"\u0f58\u0f5a\u0f53\u0f0b\u0f58\u0f7c"},week:{dow:0,doy:6}}),e.defineLocale("br",{months:"Genver_C'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split("_"),monthsShort:"Gen_C'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split("_"),weekdays:"Sul_Lun_Meurzh_Merc'her_Yaou_Gwener_Sadorn".split("_"),weekdaysShort:"Sul_Lun_Meu_Mer_Yao_Gwe_Sad".split("_"),weekdaysMin:"Su_Lu_Me_Mer_Ya_Gw_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h[e]mm A",LTS:"h[e]mm:ss A",L:"DD/MM/YYYY",LL:"D [a viz] MMMM YYYY",LLL:"D [a viz] MMMM YYYY h[e]mm A",LLLL:"dddd, D [a viz] MMMM YYYY h[e]mm A"},calendar:{sameDay:"[Hiziv da] LT",nextDay:"[Warc'hoazh da] LT",nextWeek:"dddd [da] LT",lastDay:"[Dec'h da] LT",lastWeek:"dddd [paset da] LT",sameElse:"L"},relativeTime:{future:"a-benn %s",past:"%s 'zo",s:"un nebeud segondenno\xf9",ss:"%d eilenn",m:"ur vunutenn",mm:Qe,h:"un eur",hh:"%d eur",d:"un devezh",dd:Qe,M:"ur miz",MM:Qe,y:"ur bloaz",yy:function(e){switch(Xe(e)){case 1:case 3:case 4:case 5:case 9:return e+" bloaz";default:return e+" vloaz"}}},dayOfMonthOrdinalParse:/\d{1,2}(a\xf1|vet)/,ordinal:function(e){return e+(1===e?"a\xf1":"vet")},week:{dow:1,doy:4}}),e.defineLocale("bs",{months:"januar_februar_mart_april_maj_juni_juli_august_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._aug._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[pro\u0161lu] dddd [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:ea,m:ea,mm:ea,h:ea,hh:ea,d:"dan",dd:ea,M:"mjesec",MM:ea,y:"godinu",yy:ea},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("ca",{months:{standalone:"gener_febrer_mar\xe7_abril_maig_juny_juliol_agost_setembre_octubre_novembre_desembre".split("_"),format:"de gener_de febrer_de mar\xe7_d'abril_de maig_de juny_de juliol_d'agost_de setembre_d'octubre_de novembre_de desembre".split("_"),isFormat:/D[oD]?(\s)+MMMM/},monthsShort:"gen._febr._mar\xe7_abr._maig_juny_jul._ag._set._oct._nov._des.".split("_"),monthsParseExact:!0,weekdays:"diumenge_dilluns_dimarts_dimecres_dijous_divendres_dissabte".split("_"),weekdaysShort:"dg._dl._dt._dc._dj._dv._ds.".split("_"),weekdaysMin:"dg_dl_dt_dc_dj_dv_ds".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [de] YYYY",ll:"D MMM YYYY",LLL:"D MMMM [de] YYYY [a les] H:mm",lll:"D MMM YYYY, H:mm",LLLL:"dddd D MMMM [de] YYYY [a les] H:mm",llll:"ddd D MMM YYYY, H:mm"},calendar:{sameDay:function(){return"[avui a "+(1!==this.hours()?"les":"la")+"] LT"},nextDay:function(){return"[dem\xe0 a "+(1!==this.hours()?"les":"la")+"] LT"},nextWeek:function(){return"dddd [a "+(1!==this.hours()?"les":"la")+"] LT"},lastDay:function(){return"[ahir a "+(1!==this.hours()?"les":"la")+"] LT"},lastWeek:function(){return"[el] dddd [passat a "+(1!==this.hours()?"les":"la")+"] LT"},sameElse:"L"},relativeTime:{future:"d'aqu\xed %s",past:"fa %s",s:"uns segons",ss:"%d segons",m:"un minut",mm:"%d minuts",h:"una hora",hh:"%d hores",d:"un dia",dd:"%d dies",M:"un mes",MM:"%d mesos",y:"un any",yy:"%d anys"},dayOfMonthOrdinalParse:/\d{1,2}(r|n|t|\xe8|a)/,ordinal:function(e,a){var t=1===e?"r":2===e?"n":3===e?"r":4===e?"t":"\xe8";return"w"!==a&&"W"!==a||(t="a"),e+t},week:{dow:1,doy:4}});var Vs="leden_\xfanor_b\u0159ezen_duben_kv\u011bten_\u010derven_\u010dervenec_srpen_z\xe1\u0159\xed_\u0159\xedjen_listopad_prosinec".split("_"),Ks="led_\xfano_b\u0159e_dub_kv\u011b_\u010dvn_\u010dvc_srp_z\xe1\u0159_\u0159\xedj_lis_pro".split("_");e.defineLocale("cs",{months:Vs,monthsShort:Ks,monthsParse:function(e,a){var t,s=[];for(t=0;t<12;t++)s[t]=new RegExp("^"+e[t]+"$|^"+a[t]+"$","i");return s}(Vs,Ks),shortMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(Ks),longMonthsParse:function(e){var a,t=[];for(a=0;a<12;a++)t[a]=new RegExp("^"+e[a]+"$","i");return t}(Vs),weekdays:"ned\u011ble_pond\u011bl\xed_\xfater\xfd_st\u0159eda_\u010dtvrtek_p\xe1tek_sobota".split("_"),weekdaysShort:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),weekdaysMin:"ne_po_\xfat_st_\u010dt_p\xe1_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm",l:"D. M. YYYY"},calendar:{sameDay:"[dnes v] LT",nextDay:"[z\xedtra v] LT",nextWeek:function(){switch(this.day()){case 0:return"[v ned\u011bli v] LT";case 1:case 2:return"[v] dddd [v] LT";case 3:return"[ve st\u0159edu v] LT";case 4:return"[ve \u010dtvrtek v] LT";case 5:return"[v p\xe1tek v] LT";case 6:return"[v sobotu v] LT"}},lastDay:"[v\u010dera v] LT",lastWeek:function(){switch(this.day()){case 0:return"[minulou ned\u011bli v] LT";case 1:case 2:return"[minul\xe9] dddd [v] LT";case 3:return"[minulou st\u0159edu v] LT";case 4:case 5:return"[minul\xfd] dddd [v] LT";case 6:return"[minulou sobotu v] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"p\u0159ed %s",s:ta,ss:ta,m:ta,mm:ta,h:ta,hh:ta,d:ta,dd:ta,M:ta,MM:ta,y:ta,yy:ta},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("cv",{months:"\u043a\u04d1\u0440\u043b\u0430\u0447_\u043d\u0430\u0440\u04d1\u0441_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440\u0442\u043c\u0435_\u0443\u0442\u04d1_\u04ab\u0443\u0440\u043b\u0430_\u0430\u0432\u04d1\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448\u0442\u0430\u0432".split("_"),monthsShort:"\u043a\u04d1\u0440_\u043d\u0430\u0440_\u043f\u0443\u0448_\u0430\u043a\u0430_\u043c\u0430\u0439_\u04ab\u04d7\u0440_\u0443\u0442\u04d1_\u04ab\u0443\u0440_\u0430\u0432\u043d_\u044e\u043f\u0430_\u0447\u04f3\u043a_\u0440\u0430\u0448".split("_"),weekdays:"\u0432\u044b\u0440\u0441\u0430\u0440\u043d\u0438\u043a\u0443\u043d_\u0442\u0443\u043d\u0442\u0438\u043a\u0443\u043d_\u044b\u0442\u043b\u0430\u0440\u0438\u043a\u0443\u043d_\u044e\u043d\u043a\u0443\u043d_\u043a\u04d7\u04ab\u043d\u0435\u0440\u043d\u0438\u043a\u0443\u043d_\u044d\u0440\u043d\u0435\u043a\u0443\u043d_\u0448\u04d1\u043c\u0430\u0442\u043a\u0443\u043d".split("_"),weekdaysShort:"\u0432\u044b\u0440_\u0442\u0443\u043d_\u044b\u0442\u043b_\u044e\u043d_\u043a\u04d7\u04ab_\u044d\u0440\u043d_\u0448\u04d1\u043c".split("_"),weekdaysMin:"\u0432\u0440_\u0442\u043d_\u044b\u0442_\u044e\u043d_\u043a\u04ab_\u044d\u0440_\u0448\u043c".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7]",LLL:"YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm",LLLL:"dddd, YYYY [\u04ab\u0443\u043b\u0445\u0438] MMMM [\u0443\u0439\u04d1\u0445\u04d7\u043d] D[-\u043c\u04d7\u0448\u04d7], HH:mm"},calendar:{sameDay:"[\u041f\u0430\u044f\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextDay:"[\u042b\u0440\u0430\u043d] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastDay:"[\u04d6\u043d\u0435\u0440] LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",nextWeek:"[\u04aa\u0438\u0442\u0435\u0441] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",lastWeek:"[\u0418\u0440\u0442\u043d\u04d7] dddd LT [\u0441\u0435\u0445\u0435\u0442\u0440\u0435]",sameElse:"L"},relativeTime:{future:function(e){return e+(/\u0441\u0435\u0445\u0435\u0442$/i.exec(e)?"\u0440\u0435\u043d":/\u04ab\u0443\u043b$/i.exec(e)?"\u0442\u0430\u043d":"\u0440\u0430\u043d")},past:"%s \u043a\u0430\u044f\u043b\u043b\u0430",s:"\u043f\u04d7\u0440-\u0438\u043a \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",ss:"%d \u04ab\u0435\u043a\u043a\u0443\u043d\u0442",m:"\u043f\u04d7\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u043f\u04d7\u0440 \u0441\u0435\u0445\u0435\u0442",hh:"%d \u0441\u0435\u0445\u0435\u0442",d:"\u043f\u04d7\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u043f\u04d7\u0440 \u0443\u0439\u04d1\u0445",MM:"%d \u0443\u0439\u04d1\u0445",y:"\u043f\u04d7\u0440 \u04ab\u0443\u043b",yy:"%d \u04ab\u0443\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-\u043c\u04d7\u0448/,ordinal:"%d-\u043c\u04d7\u0448",week:{dow:1,doy:7}}),e.defineLocale("cy",{months:"Ionawr_Chwefror_Mawrth_Ebrill_Mai_Mehefin_Gorffennaf_Awst_Medi_Hydref_Tachwedd_Rhagfyr".split("_"),monthsShort:"Ion_Chwe_Maw_Ebr_Mai_Meh_Gor_Aws_Med_Hyd_Tach_Rhag".split("_"),weekdays:"Dydd Sul_Dydd Llun_Dydd Mawrth_Dydd Mercher_Dydd Iau_Dydd Gwener_Dydd Sadwrn".split("_"),weekdaysShort:"Sul_Llun_Maw_Mer_Iau_Gwe_Sad".split("_"),weekdaysMin:"Su_Ll_Ma_Me_Ia_Gw_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Heddiw am] LT",nextDay:"[Yfory am] LT",nextWeek:"dddd [am] LT",lastDay:"[Ddoe am] LT",lastWeek:"dddd [diwethaf am] LT",sameElse:"L"},relativeTime:{future:"mewn %s",past:"%s yn \xf4l",s:"ychydig eiliadau",ss:"%d eiliad",m:"munud",mm:"%d munud",h:"awr",hh:"%d awr",d:"diwrnod",dd:"%d diwrnod",M:"mis",MM:"%d mis",y:"blwyddyn",yy:"%d flynedd"},dayOfMonthOrdinalParse:/\d{1,2}(fed|ain|af|il|ydd|ed|eg)/,ordinal:function(e){var a="";return e>20?a=40===e||50===e||60===e||80===e||100===e?"fed":"ain":e>0&&(a=["","af","il","ydd","ydd","ed","ed","ed","fed","fed","fed","eg","fed","eg","eg","fed","eg","eg","fed","eg","fed"][e]),e+a},week:{dow:1,doy:4}}),e.defineLocale("da",{months:"januar_februar_marts_april_maj_juni_juli_august_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8n_man_tir_ons_tor_fre_l\xf8r".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd [d.] D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"p\xe5 dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[i] dddd[s kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"f\xe5 sekunder",ss:"%d sekunder",m:"et minut",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dage",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"et \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de-at",{months:"J\xe4nner_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"J\xe4n._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:sa,mm:"%d Minuten",h:sa,hh:"%d Stunden",d:sa,dd:sa,M:sa,MM:sa,y:sa,yy:sa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de-ch",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:na,mm:"%d Minuten",h:na,hh:"%d Stunden",d:na,dd:na,M:na,MM:na,y:na,yy:na},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("de",{months:"Januar_Februar_M\xe4rz_April_Mai_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Feb._M\xe4rz_Apr._Mai_Juni_Juli_Aug._Sep._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonntag_Montag_Dienstag_Mittwoch_Donnerstag_Freitag_Samstag".split("_"),weekdaysShort:"So._Mo._Di._Mi._Do._Fr._Sa.".split("_"),weekdaysMin:"So_Mo_Di_Mi_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY HH:mm",LLLL:"dddd, D. MMMM YYYY HH:mm"},calendar:{sameDay:"[heute um] LT [Uhr]",sameElse:"L",nextDay:"[morgen um] LT [Uhr]",nextWeek:"dddd [um] LT [Uhr]",lastDay:"[gestern um] LT [Uhr]",lastWeek:"[letzten] dddd [um] LT [Uhr]"},relativeTime:{future:"in %s",past:"vor %s",s:"ein paar Sekunden",ss:"%d Sekunden",m:da,mm:"%d Minuten",h:da,hh:"%d Stunden",d:da,dd:da,M:da,MM:da,y:da,yy:da},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Zs=["\u0796\u07ac\u0782\u07aa\u0787\u07a6\u0783\u07a9","\u078a\u07ac\u0784\u07b0\u0783\u07aa\u0787\u07a6\u0783\u07a9","\u0789\u07a7\u0783\u07a8\u0797\u07aa","\u0787\u07ad\u0795\u07b0\u0783\u07a9\u078d\u07aa","\u0789\u07ad","\u0796\u07ab\u0782\u07b0","\u0796\u07aa\u078d\u07a6\u0787\u07a8","\u0787\u07af\u078e\u07a6\u0790\u07b0\u0793\u07aa","\u0790\u07ac\u0795\u07b0\u0793\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0787\u07ae\u0786\u07b0\u0793\u07af\u0784\u07a6\u0783\u07aa","\u0782\u07ae\u0788\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa","\u0791\u07a8\u0790\u07ac\u0789\u07b0\u0784\u07a6\u0783\u07aa"],$s=["\u0787\u07a7\u078b\u07a8\u0787\u07b0\u078c\u07a6","\u0780\u07af\u0789\u07a6","\u0787\u07a6\u0782\u07b0\u078e\u07a7\u0783\u07a6","\u0784\u07aa\u078b\u07a6","\u0784\u07aa\u0783\u07a7\u0790\u07b0\u078a\u07a6\u078c\u07a8","\u0780\u07aa\u0786\u07aa\u0783\u07aa","\u0780\u07ae\u0782\u07a8\u0780\u07a8\u0783\u07aa"];e.defineLocale("dv",{months:Zs,monthsShort:Zs,weekdays:$s,weekdaysShort:$s,weekdaysMin:"\u0787\u07a7\u078b\u07a8_\u0780\u07af\u0789\u07a6_\u0787\u07a6\u0782\u07b0_\u0784\u07aa\u078b\u07a6_\u0784\u07aa\u0783\u07a7_\u0780\u07aa\u0786\u07aa_\u0780\u07ae\u0782\u07a8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"D/M/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0789\u0786|\u0789\u078a/,isPM:function(e){return"\u0789\u078a"===e},meridiem:function(e,a,t){return e<12?"\u0789\u0786":"\u0789\u078a"},calendar:{sameDay:"[\u0789\u07a8\u0787\u07a6\u078b\u07aa] LT",nextDay:"[\u0789\u07a7\u078b\u07a6\u0789\u07a7] LT",nextWeek:"dddd LT",lastDay:"[\u0787\u07a8\u0787\u07b0\u0794\u07ac] LT",lastWeek:"[\u078a\u07a7\u0787\u07a8\u078c\u07aa\u0788\u07a8] dddd LT",sameElse:"L"},relativeTime:{future:"\u078c\u07ac\u0783\u07ad\u078e\u07a6\u0787\u07a8 %s",past:"\u0786\u07aa\u0783\u07a8\u0782\u07b0 %s",s:"\u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa\u0786\u07ae\u0785\u07ac\u0787\u07b0",ss:"d% \u0790\u07a8\u0786\u07aa\u0782\u07b0\u078c\u07aa",m:"\u0789\u07a8\u0782\u07a8\u0793\u07ac\u0787\u07b0",mm:"\u0789\u07a8\u0782\u07a8\u0793\u07aa %d",h:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07ac\u0787\u07b0",hh:"\u078e\u07a6\u0791\u07a8\u0787\u07a8\u0783\u07aa %d",d:"\u078b\u07aa\u0788\u07a6\u0780\u07ac\u0787\u07b0",dd:"\u078b\u07aa\u0788\u07a6\u0790\u07b0 %d",M:"\u0789\u07a6\u0780\u07ac\u0787\u07b0",MM:"\u0789\u07a6\u0790\u07b0 %d",y:"\u0787\u07a6\u0780\u07a6\u0783\u07ac\u0787\u07b0",yy:"\u0787\u07a6\u0780\u07a6\u0783\u07aa %d"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:7,doy:12}}),e.defineLocale("el",{monthsNominativeEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2_\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2_\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2_\u039c\u03ac\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2_\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2_\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2_\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2_\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2_\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2".split("_"),monthsGenitiveEl:"\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5_\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5_\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5_\u039c\u03b1\u0390\u03bf\u03c5_\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5_\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5_\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5_\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5_\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5_\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5".split("_"),months:function(e,a){return e?"string"==typeof a&&/D/.test(a.substring(0,a.indexOf("MMMM")))?this._monthsGenitiveEl[e.month()]:this._monthsNominativeEl[e.month()]:this._monthsNominativeEl},monthsShort:"\u0399\u03b1\u03bd_\u03a6\u03b5\u03b2_\u039c\u03b1\u03c1_\u0391\u03c0\u03c1_\u039c\u03b1\u03ca_\u0399\u03bf\u03c5\u03bd_\u0399\u03bf\u03c5\u03bb_\u0391\u03c5\u03b3_\u03a3\u03b5\u03c0_\u039f\u03ba\u03c4_\u039d\u03bf\u03b5_\u0394\u03b5\u03ba".split("_"),weekdays:"\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae_\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1_\u03a4\u03c1\u03af\u03c4\u03b7_\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7_\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7_\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae_\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf".split("_"),weekdaysShort:"\u039a\u03c5\u03c1_\u0394\u03b5\u03c5_\u03a4\u03c1\u03b9_\u03a4\u03b5\u03c4_\u03a0\u03b5\u03bc_\u03a0\u03b1\u03c1_\u03a3\u03b1\u03b2".split("_"),weekdaysMin:"\u039a\u03c5_\u0394\u03b5_\u03a4\u03c1_\u03a4\u03b5_\u03a0\u03b5_\u03a0\u03b1_\u03a3\u03b1".split("_"),meridiem:function(e,a,t){return e>11?t?"\u03bc\u03bc":"\u039c\u039c":t?"\u03c0\u03bc":"\u03a0\u039c"},isPM:function(e){return"\u03bc"===(e+"").toLowerCase()[0]},meridiemParse:/[\u03a0\u039c]\.?\u039c?\.?/i,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendarEl:{sameDay:"[\u03a3\u03ae\u03bc\u03b5\u03c1\u03b1 {}] LT",nextDay:"[\u0391\u03cd\u03c1\u03b9\u03bf {}] LT",nextWeek:"dddd [{}] LT",lastDay:"[\u03a7\u03b8\u03b5\u03c2 {}] LT",lastWeek:function(){switch(this.day()){case 6:return"[\u03c4\u03bf \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf] dddd [{}] LT";default:return"[\u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7] dddd [{}] LT"}},sameElse:"L"},calendar:function(e,a){var t=this._calendarEl[e],s=a&&a.hours();return D(t)&&(t=t.apply(a)),t.replace("{}",s%12==1?"\u03c3\u03c4\u03b7":"\u03c3\u03c4\u03b9\u03c2")},relativeTime:{future:"\u03c3\u03b5 %s",past:"%s \u03c0\u03c1\u03b9\u03bd",s:"\u03bb\u03af\u03b3\u03b1 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",ss:"%d \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03cc\u03bb\u03b5\u03c0\u03c4\u03b1",m:"\u03ad\u03bd\u03b1 \u03bb\u03b5\u03c0\u03c4\u03cc",mm:"%d \u03bb\u03b5\u03c0\u03c4\u03ac",h:"\u03bc\u03af\u03b1 \u03ce\u03c1\u03b1",hh:"%d \u03ce\u03c1\u03b5\u03c2",d:"\u03bc\u03af\u03b1 \u03bc\u03ad\u03c1\u03b1",dd:"%d \u03bc\u03ad\u03c1\u03b5\u03c2",M:"\u03ad\u03bd\u03b1\u03c2 \u03bc\u03ae\u03bd\u03b1\u03c2",MM:"%d \u03bc\u03ae\u03bd\u03b5\u03c2",y:"\u03ad\u03bd\u03b1\u03c2 \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2",yy:"%d \u03c7\u03c1\u03cc\u03bd\u03b9\u03b1"},dayOfMonthOrdinalParse:/\d{1,2}\u03b7/,ordinal:"%d\u03b7",week:{dow:1,doy:4}}),e.defineLocale("en-au",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-ca",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"YYYY-MM-DD",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")}}),e.defineLocale("en-gb",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-ie",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("en-nz",{months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("eo",{months:"januaro_februaro_marto_aprilo_majo_junio_julio_a\u016dgusto_septembro_oktobro_novembro_decembro".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_a\u016dg_sep_okt_nov_dec".split("_"),weekdays:"diman\u0109o_lundo_mardo_merkredo_\u0135a\u016ddo_vendredo_sabato".split("_"),weekdaysShort:"dim_lun_mard_merk_\u0135a\u016d_ven_sab".split("_"),weekdaysMin:"di_lu_ma_me_\u0135a_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D[-a de] MMMM, YYYY",LLL:"D[-a de] MMMM, YYYY HH:mm",LLLL:"dddd, [la] D[-a de] MMMM, YYYY HH:mm"},meridiemParse:/[ap]\.t\.m/i,isPM:function(e){return"p"===e.charAt(0).toLowerCase()},meridiem:function(e,a,t){return e>11?t?"p.t.m.":"P.T.M.":t?"a.t.m.":"A.T.M."},calendar:{sameDay:"[Hodia\u016d je] LT",nextDay:"[Morga\u016d je] LT",nextWeek:"dddd [je] LT",lastDay:"[Hiera\u016d je] LT",lastWeek:"[pasinta] dddd [je] LT",sameElse:"L"},relativeTime:{future:"post %s",past:"anta\u016d %s",s:"sekundoj",ss:"%d sekundoj",m:"minuto",mm:"%d minutoj",h:"horo",hh:"%d horoj",d:"tago",dd:"%d tagoj",M:"monato",MM:"%d monatoj",y:"jaro",yy:"%d jaroj"},dayOfMonthOrdinalParse:/\d{1,2}a/,ordinal:"%da",week:{dow:1,doy:7}});var Bs="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),qs="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),Qs=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],Xs=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;e.defineLocale("es-do",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?qs[e.month()]:Bs[e.month()]:Bs},monthsRegex:Xs,monthsShortRegex:Xs,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:Qs,longMonthsParse:Qs,shortMonthsParse:Qs,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY h:mm A",LLLL:"dddd, D [de] MMMM [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}});var en="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),an="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");e.defineLocale("es-us",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?an[e.month()]:en[e.month()]:en},monthsParseExact:!0,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"MM/DD/YYYY",LL:"MMMM [de] D [de] YYYY",LLL:"MMMM [de] D [de] YYYY h:mm A",LLLL:"dddd, MMMM [de] D [de] YYYY h:mm A"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:0,doy:6}});var tn="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),sn="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_"),nn=[/^ene/i,/^feb/i,/^mar/i,/^abr/i,/^may/i,/^jun/i,/^jul/i,/^ago/i,/^sep/i,/^oct/i,/^nov/i,/^dic/i],dn=/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre|ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i;e.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?sn[e.month()]:tn[e.month()]:tn},monthsRegex:dn,monthsShortRegex:dn,monthsStrictRegex:/^(enero|febrero|marzo|abril|mayo|junio|julio|agosto|septiembre|octubre|noviembre|diciembre)/i,monthsShortStrictRegex:/^(ene\.?|feb\.?|mar\.?|abr\.?|may\.?|jun\.?|jul\.?|ago\.?|sep\.?|oct\.?|nov\.?|dic\.?)/i,monthsParse:nn,longMonthsParse:nn,shortMonthsParse:nn,weekdays:"domingo_lunes_martes_mi\xe9rcoles_jueves_viernes_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._mi\xe9._jue._vie._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[ma\xf1ana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un a\xf1o",yy:"%d a\xf1os"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("et",{months:"jaanuar_veebruar_m\xe4rts_aprill_mai_juuni_juuli_august_september_oktoober_november_detsember".split("_"),monthsShort:"jaan_veebr_m\xe4rts_apr_mai_juuni_juuli_aug_sept_okt_nov_dets".split("_"),weekdays:"p\xfchap\xe4ev_esmasp\xe4ev_teisip\xe4ev_kolmap\xe4ev_neljap\xe4ev_reede_laup\xe4ev".split("_"),weekdaysShort:"P_E_T_K_N_R_L".split("_"),weekdaysMin:"P_E_T_K_N_R_L".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[T\xe4na,] LT",nextDay:"[Homme,] LT",nextWeek:"[J\xe4rgmine] dddd LT",lastDay:"[Eile,] LT",lastWeek:"[Eelmine] dddd LT",sameElse:"L"},relativeTime:{future:"%s p\xe4rast",past:"%s tagasi",s:ra,ss:ra,m:ra,mm:ra,h:ra,hh:ra,d:ra,dd:"%d p\xe4eva",M:ra,MM:ra,y:ra,yy:ra},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("eu",{months:"urtarrila_otsaila_martxoa_apirila_maiatza_ekaina_uztaila_abuztua_iraila_urria_azaroa_abendua".split("_"),monthsShort:"urt._ots._mar._api._mai._eka._uzt._abu._ira._urr._aza._abe.".split("_"),monthsParseExact:!0,weekdays:"igandea_astelehena_asteartea_asteazkena_osteguna_ostirala_larunbata".split("_"),weekdaysShort:"ig._al._ar._az._og._ol._lr.".split("_"),weekdaysMin:"ig_al_ar_az_og_ol_lr".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY[ko] MMMM[ren] D[a]",LLL:"YYYY[ko] MMMM[ren] D[a] HH:mm",LLLL:"dddd, YYYY[ko] MMMM[ren] D[a] HH:mm",l:"YYYY-M-D",ll:"YYYY[ko] MMM D[a]",lll:"YYYY[ko] MMM D[a] HH:mm",llll:"ddd, YYYY[ko] MMM D[a] HH:mm"},calendar:{sameDay:"[gaur] LT[etan]",nextDay:"[bihar] LT[etan]",nextWeek:"dddd LT[etan]",lastDay:"[atzo] LT[etan]",lastWeek:"[aurreko] dddd LT[etan]",sameElse:"L"},relativeTime:{future:"%s barru",past:"duela %s",s:"segundo batzuk",ss:"%d segundo",m:"minutu bat",mm:"%d minutu",h:"ordu bat",hh:"%d ordu",d:"egun bat",dd:"%d egun",M:"hilabete bat",MM:"%d hilabete",y:"urte bat",yy:"%d urte"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var rn={1:"\u06f1",2:"\u06f2",3:"\u06f3",4:"\u06f4",5:"\u06f5",6:"\u06f6",7:"\u06f7",8:"\u06f8",9:"\u06f9",0:"\u06f0"},_n={"\u06f1":"1","\u06f2":"2","\u06f3":"3","\u06f4":"4","\u06f5":"5","\u06f6":"6","\u06f7":"7","\u06f8":"8","\u06f9":"9","\u06f0":"0"};e.defineLocale("fa",{months:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),monthsShort:"\u0698\u0627\u0646\u0648\u06cc\u0647_\u0641\u0648\u0631\u06cc\u0647_\u0645\u0627\u0631\u0633_\u0622\u0648\u0631\u06cc\u0644_\u0645\u0647_\u0698\u0648\u0626\u0646_\u0698\u0648\u0626\u06cc\u0647_\u0627\u0648\u062a_\u0633\u067e\u062a\u0627\u0645\u0628\u0631_\u0627\u06a9\u062a\u0628\u0631_\u0646\u0648\u0627\u0645\u0628\u0631_\u062f\u0633\u0627\u0645\u0628\u0631".split("_"),weekdays:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysShort:"\u06cc\u06a9\u200c\u0634\u0646\u0628\u0647_\u062f\u0648\u0634\u0646\u0628\u0647_\u0633\u0647\u200c\u0634\u0646\u0628\u0647_\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647_\u067e\u0646\u062c\u200c\u0634\u0646\u0628\u0647_\u062c\u0645\u0639\u0647_\u0634\u0646\u0628\u0647".split("_"),weekdaysMin:"\u06cc_\u062f_\u0633_\u0686_\u067e_\u062c_\u0634".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},meridiemParse:/\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631|\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/,isPM:function(e){return/\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631/.test(e)},meridiem:function(e,a,t){return e<12?"\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631":"\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631"},calendar:{sameDay:"[\u0627\u0645\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",nextDay:"[\u0641\u0631\u062f\u0627 \u0633\u0627\u0639\u062a] LT",nextWeek:"dddd [\u0633\u0627\u0639\u062a] LT",lastDay:"[\u062f\u06cc\u0631\u0648\u0632 \u0633\u0627\u0639\u062a] LT",lastWeek:"dddd [\u067e\u06cc\u0634] [\u0633\u0627\u0639\u062a] LT",sameElse:"L"},relativeTime:{future:"\u062f\u0631 %s",past:"%s \u067e\u06cc\u0634",s:"\u0686\u0646\u062f \u062b\u0627\u0646\u06cc\u0647",ss:"\u062b\u0627\u0646\u06cc\u0647 d%",m:"\u06cc\u06a9 \u062f\u0642\u06cc\u0642\u0647",mm:"%d \u062f\u0642\u06cc\u0642\u0647",h:"\u06cc\u06a9 \u0633\u0627\u0639\u062a",hh:"%d \u0633\u0627\u0639\u062a",d:"\u06cc\u06a9 \u0631\u0648\u0632",dd:"%d \u0631\u0648\u0632",M:"\u06cc\u06a9 \u0645\u0627\u0647",MM:"%d \u0645\u0627\u0647",y:"\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/[\u06f0-\u06f9]/g,function(e){return _n[e]}).replace(/\u060c/g,",")},postformat:function(e){return e.replace(/\d/g,function(e){return rn[e]}).replace(/,/g,"\u060c")},dayOfMonthOrdinalParse:/\d{1,2}\u0645/,ordinal:"%d\u0645",week:{dow:6,doy:12}});var on="nolla yksi kaksi kolme nelj\xe4 viisi kuusi seitsem\xe4n kahdeksan yhdeks\xe4n".split(" "),mn=["nolla","yhden","kahden","kolmen","nelj\xe4n","viiden","kuuden",on[7],on[8],on[9]];e.defineLocale("fi",{months:"tammikuu_helmikuu_maaliskuu_huhtikuu_toukokuu_kes\xe4kuu_hein\xe4kuu_elokuu_syyskuu_lokakuu_marraskuu_joulukuu".split("_"),monthsShort:"tammi_helmi_maalis_huhti_touko_kes\xe4_hein\xe4_elo_syys_loka_marras_joulu".split("_"),weekdays:"sunnuntai_maanantai_tiistai_keskiviikko_torstai_perjantai_lauantai".split("_"),weekdaysShort:"su_ma_ti_ke_to_pe_la".split("_"),weekdaysMin:"su_ma_ti_ke_to_pe_la".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"Do MMMM[ta] YYYY",LLL:"Do MMMM[ta] YYYY, [klo] HH.mm",LLLL:"dddd, Do MMMM[ta] YYYY, [klo] HH.mm",l:"D.M.YYYY",ll:"Do MMM YYYY",lll:"Do MMM YYYY, [klo] HH.mm",llll:"ddd, Do MMM YYYY, [klo] HH.mm"},calendar:{sameDay:"[t\xe4n\xe4\xe4n] [klo] LT",nextDay:"[huomenna] [klo] LT",nextWeek:"dddd [klo] LT",lastDay:"[eilen] [klo] LT",lastWeek:"[viime] dddd[na] [klo] LT",sameElse:"L"},relativeTime:{future:"%s p\xe4\xe4st\xe4",past:"%s sitten",s:_a,ss:_a,m:_a,mm:_a,h:_a,hh:_a,d:_a,dd:_a,M:_a,MM:_a,y:_a,yy:_a},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("fo",{months:"januar_februar_mars_apr\xedl_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sunnudagur_m\xe1nadagur_t\xfdsdagur_mikudagur_h\xf3sdagur_fr\xedggjadagur_leygardagur".split("_"),weekdaysShort:"sun_m\xe1n_t\xfds_mik_h\xf3s_fr\xed_ley".split("_"),weekdaysMin:"su_m\xe1_t\xfd_mi_h\xf3_fr_le".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D. MMMM, YYYY HH:mm"},calendar:{sameDay:"[\xcd dag kl.] LT",nextDay:"[\xcd morgin kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xcd gj\xe1r kl.] LT",lastWeek:"[s\xed\xf0stu] dddd [kl] LT",sameElse:"L"},relativeTime:{future:"um %s",past:"%s s\xed\xf0ani",s:"f\xe1 sekund",ss:"%d sekundir",m:"ein minutt",mm:"%d minuttir",h:"ein t\xedmi",hh:"%d t\xedmar",d:"ein dagur",dd:"%d dagar",M:"ein m\xe1na\xf0i",MM:"%d m\xe1na\xf0ir",y:"eitt \xe1r",yy:"%d \xe1r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("fr-ca",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(e,a){switch(a){default:case"M":case"Q":case"D":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}}}),e.defineLocale("fr-ch",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|e)/,ordinal:function(e,a){switch(a){default:case"M":case"Q":case"D":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}},week:{dow:1,doy:4}}),e.defineLocale("fr",{months:"janvier_f\xe9vrier_mars_avril_mai_juin_juillet_ao\xfbt_septembre_octobre_novembre_d\xe9cembre".split("_"),monthsShort:"janv._f\xe9vr._mars_avr._mai_juin_juil._ao\xfbt_sept._oct._nov._d\xe9c.".split("_"),monthsParseExact:!0,weekdays:"dimanche_lundi_mardi_mercredi_jeudi_vendredi_samedi".split("_"),weekdaysShort:"dim._lun._mar._mer._jeu._ven._sam.".split("_"),weekdaysMin:"di_lu_ma_me_je_ve_sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Aujourd\u2019hui \xe0] LT",nextDay:"[Demain \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[Hier \xe0] LT",lastWeek:"dddd [dernier \xe0] LT",sameElse:"L"},relativeTime:{future:"dans %s",past:"il y a %s",s:"quelques secondes",ss:"%d secondes",m:"une minute",mm:"%d minutes",h:"une heure",hh:"%d heures",d:"un jour",dd:"%d jours",M:"un mois",MM:"%d mois",y:"un an",yy:"%d ans"},dayOfMonthOrdinalParse:/\d{1,2}(er|)/,ordinal:function(e,a){switch(a){case"D":return e+(1===e?"er":"");default:case"M":case"Q":case"DDD":case"d":return e+(1===e?"er":"e");case"w":case"W":return e+(1===e?"re":"e")}},week:{dow:1,doy:4}});var un="jan._feb._mrt._apr._mai_jun._jul._aug._sep._okt._nov._des.".split("_"),ln="jan_feb_mrt_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_");e.defineLocale("fy",{months:"jannewaris_febrewaris_maart_april_maaie_juny_july_augustus_septimber_oktober_novimber_desimber".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?ln[e.month()]:un[e.month()]:un},monthsParseExact:!0,weekdays:"snein_moandei_tiisdei_woansdei_tongersdei_freed_sneon".split("_"),weekdaysShort:"si._mo._ti._wo._to._fr._so.".split("_"),weekdaysMin:"Si_Mo_Ti_Wo_To_Fr_So".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[hjoed om] LT",nextDay:"[moarn om] LT",nextWeek:"dddd [om] LT",lastDay:"[juster om] LT",lastWeek:"[\xf4fr\xfbne] dddd [om] LT",sameElse:"L"},relativeTime:{future:"oer %s",past:"%s lyn",s:"in pear sekonden",ss:"%d sekonden",m:"ien min\xfat",mm:"%d minuten",h:"ien oere",hh:"%d oeren",d:"ien dei",dd:"%d dagen",M:"ien moanne",MM:"%d moannen",y:"ien jier",yy:"%d jierren"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});e.defineLocale("gd",{months:["Am Faoilleach","An Gearran","Am M\xe0rt","An Giblean","An C\xe8itean","An t-\xd2gmhios","An t-Iuchar","An L\xf9nastal","An t-Sultain","An D\xe0mhair","An t-Samhain","An D\xf9bhlachd"],monthsShort:["Faoi","Gear","M\xe0rt","Gibl","C\xe8it","\xd2gmh","Iuch","L\xf9n","Sult","D\xe0mh","Samh","D\xf9bh"],monthsParseExact:!0,weekdays:["Did\xf2mhnaich","Diluain","Dim\xe0irt","Diciadain","Diardaoin","Dihaoine","Disathairne"],weekdaysShort:["Did","Dil","Dim","Dic","Dia","Dih","Dis"],weekdaysMin:["D\xf2","Lu","M\xe0","Ci","Ar","Ha","Sa"],longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[An-diugh aig] LT",nextDay:"[A-m\xe0ireach aig] LT",nextWeek:"dddd [aig] LT",lastDay:"[An-d\xe8 aig] LT",lastWeek:"dddd [seo chaidh] [aig] LT",sameElse:"L"},relativeTime:{future:"ann an %s",past:"bho chionn %s",s:"beagan diogan",ss:"%d diogan",m:"mionaid",mm:"%d mionaidean",h:"uair",hh:"%d uairean",d:"latha",dd:"%d latha",M:"m\xecos",MM:"%d m\xecosan",y:"bliadhna",yy:"%d bliadhna"},dayOfMonthOrdinalParse:/\d{1,2}(d|na|mh)/,ordinal:function(e){return e+(1===e?"d":e%10==2?"na":"mh")},week:{dow:1,doy:4}}),e.defineLocale("gl",{months:"xaneiro_febreiro_marzo_abril_maio_xu\xf1o_xullo_agosto_setembro_outubro_novembro_decembro".split("_"),monthsShort:"xan._feb._mar._abr._mai._xu\xf1._xul._ago._set._out._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"domingo_luns_martes_m\xe9rcores_xoves_venres_s\xe1bado".split("_"),weekdaysShort:"dom._lun._mar._m\xe9r._xov._ven._s\xe1b.".split("_"),weekdaysMin:"do_lu_ma_m\xe9_xo_ve_s\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoxe "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextDay:function(){return"[ma\xf1\xe1 "+(1!==this.hours()?"\xe1s":"\xe1")+"] LT"},nextWeek:function(){return"dddd ["+(1!==this.hours()?"\xe1s":"a")+"] LT"},lastDay:function(){return"[onte "+(1!==this.hours()?"\xe1":"a")+"] LT"},lastWeek:function(){return"[o] dddd [pasado "+(1!==this.hours()?"\xe1s":"a")+"] LT"},sameElse:"L"},relativeTime:{future:function(e){return 0===e.indexOf("un")?"n"+e:"en "+e},past:"hai %s",s:"uns segundos",ss:"%d segundos",m:"un minuto",mm:"%d minutos",h:"unha hora",hh:"%d horas",d:"un d\xeda",dd:"%d d\xedas",M:"un mes",MM:"%d meses",y:"un ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("gom-latn",{months:"Janer_Febrer_Mars_Abril_Mai_Jun_Julai_Agost_Setembr_Otubr_Novembr_Dezembr".split("_"),monthsShort:"Jan._Feb._Mars_Abr._Mai_Jun_Jul._Ago._Set._Otu._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Aitar_Somar_Mongllar_Budvar_Brestar_Sukrar_Son'var".split("_"),weekdaysShort:"Ait._Som._Mon._Bud._Bre._Suk._Son.".split("_"),weekdaysMin:"Ai_Sm_Mo_Bu_Br_Su_Sn".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A h:mm [vazta]",LTS:"A h:mm:ss [vazta]",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY A h:mm [vazta]",LLLL:"dddd, MMMM[achea] Do, YYYY, A h:mm [vazta]",llll:"ddd, D MMM YYYY, A h:mm [vazta]"},calendar:{sameDay:"[Aiz] LT",nextDay:"[Faleam] LT",nextWeek:"[Ieta to] dddd[,] LT",lastDay:"[Kal] LT",lastWeek:"[Fatlo] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s",past:"%s adim",s:ia,ss:ia,m:ia,mm:ia,h:ia,hh:ia,d:ia,dd:ia,M:ia,MM:ia,y:ia,yy:ia},dayOfMonthOrdinalParse:/\d{1,2}(er)/,ordinal:function(e,a){switch(a){case"D":return e+"er";default:case"M":case"Q":case"DDD":case"d":case"w":case"W":return e}},week:{dow:1,doy:4},meridiemParse:/rati|sokalli|donparam|sanje/,meridiemHour:function(e,a){return 12===e&&(e=0),"rati"===a?e<4?e:e+12:"sokalli"===a?e:"donparam"===a?e>12?e:e+12:"sanje"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"rati":e<12?"sokalli":e<16?"donparam":e<20?"sanje":"rati"}});var Mn={1:"\u0ae7",2:"\u0ae8",3:"\u0ae9",4:"\u0aea",5:"\u0aeb",6:"\u0aec",7:"\u0aed",8:"\u0aee",9:"\u0aef",0:"\u0ae6"},hn={"\u0ae7":"1","\u0ae8":"2","\u0ae9":"3","\u0aea":"4","\u0aeb":"5","\u0aec":"6","\u0aed":"7","\u0aee":"8","\u0aef":"9","\u0ae6":"0"};e.defineLocale("gu",{months:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1\u0a86\u0ab0\u0ac0_\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1\u0a86\u0ab0\u0ac0_\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2_\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe\u0a88_\u0a91\u0a97\u0ab8\u0acd\u0a9f_\u0ab8\u0aaa\u0acd\u0a9f\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0a91\u0a95\u0acd\u0a9f\u0acd\u0aac\u0ab0_\u0aa8\u0ab5\u0ac7\u0aae\u0acd\u0aac\u0ab0_\u0aa1\u0abf\u0ab8\u0ac7\u0aae\u0acd\u0aac\u0ab0".split("_"),monthsShort:"\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1._\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1._\u0aae\u0abe\u0ab0\u0acd\u0a9a_\u0a8f\u0aaa\u0acd\u0ab0\u0abf._\u0aae\u0ac7_\u0a9c\u0ac2\u0aa8_\u0a9c\u0ac1\u0ab2\u0abe._\u0a91\u0a97._\u0ab8\u0aaa\u0acd\u0a9f\u0ac7._\u0a91\u0a95\u0acd\u0a9f\u0acd._\u0aa8\u0ab5\u0ac7._\u0aa1\u0abf\u0ab8\u0ac7.".split("_"),monthsParseExact:!0,weekdays:"\u0ab0\u0ab5\u0abf\u0ab5\u0abe\u0ab0_\u0ab8\u0acb\u0aae\u0ab5\u0abe\u0ab0_\u0aae\u0a82\u0a97\u0ab3\u0ab5\u0abe\u0ab0_\u0aac\u0ac1\u0aa7\u0acd\u0ab5\u0abe\u0ab0_\u0a97\u0ac1\u0ab0\u0ac1\u0ab5\u0abe\u0ab0_\u0ab6\u0ac1\u0a95\u0acd\u0ab0\u0ab5\u0abe\u0ab0_\u0ab6\u0aa8\u0abf\u0ab5\u0abe\u0ab0".split("_"),weekdaysShort:"\u0ab0\u0ab5\u0abf_\u0ab8\u0acb\u0aae_\u0aae\u0a82\u0a97\u0ab3_\u0aac\u0ac1\u0aa7\u0acd_\u0a97\u0ac1\u0ab0\u0ac1_\u0ab6\u0ac1\u0a95\u0acd\u0ab0_\u0ab6\u0aa8\u0abf".split("_"),weekdaysMin:"\u0ab0_\u0ab8\u0acb_\u0aae\u0a82_\u0aac\u0ac1_\u0a97\u0ac1_\u0ab6\u0ac1_\u0ab6".split("_"),longDateFormat:{LT:"A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LTS:"A h:mm:ss \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7",LLLL:"dddd, D MMMM YYYY, A h:mm \u0ab5\u0abe\u0a97\u0acd\u0aaf\u0ac7"},calendar:{sameDay:"[\u0a86\u0a9c] LT",nextDay:"[\u0a95\u0abe\u0ab2\u0ac7] LT",nextWeek:"dddd, LT",lastDay:"[\u0a97\u0a87\u0a95\u0abe\u0ab2\u0ac7] LT",lastWeek:"[\u0aaa\u0abe\u0a9b\u0ab2\u0abe] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0aae\u0abe",past:"%s \u0aaa\u0ac7\u0ab9\u0ab2\u0abe",s:"\u0a85\u0aae\u0ac1\u0a95 \u0aaa\u0ab3\u0acb",ss:"%d \u0ab8\u0ac7\u0a95\u0a82\u0aa1",m:"\u0a8f\u0a95 \u0aae\u0abf\u0aa8\u0abf\u0a9f",mm:"%d \u0aae\u0abf\u0aa8\u0abf\u0a9f",h:"\u0a8f\u0a95 \u0a95\u0ab2\u0abe\u0a95",hh:"%d \u0a95\u0ab2\u0abe\u0a95",d:"\u0a8f\u0a95 \u0aa6\u0abf\u0ab5\u0ab8",dd:"%d \u0aa6\u0abf\u0ab5\u0ab8",M:"\u0a8f\u0a95 \u0aae\u0ab9\u0abf\u0aa8\u0acb",MM:"%d \u0aae\u0ab9\u0abf\u0aa8\u0acb",y:"\u0a8f\u0a95 \u0ab5\u0ab0\u0acd\u0ab7",yy:"%d \u0ab5\u0ab0\u0acd\u0ab7"},preparse:function(e){return e.replace(/[\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u0ae6]/g,function(e){return hn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Mn[e]})},meridiemParse:/\u0ab0\u0abe\u0aa4|\u0aac\u0aaa\u0acb\u0ab0|\u0ab8\u0ab5\u0abe\u0ab0|\u0ab8\u0abe\u0a82\u0a9c/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0ab0\u0abe\u0aa4"===a?e<4?e:e+12:"\u0ab8\u0ab5\u0abe\u0ab0"===a?e:"\u0aac\u0aaa\u0acb\u0ab0"===a?e>=10?e:e+12:"\u0ab8\u0abe\u0a82\u0a9c"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0ab0\u0abe\u0aa4":e<10?"\u0ab8\u0ab5\u0abe\u0ab0":e<17?"\u0aac\u0aaa\u0acb\u0ab0":e<20?"\u0ab8\u0abe\u0a82\u0a9c":"\u0ab0\u0abe\u0aa4"},week:{dow:0,doy:6}}),e.defineLocale("he",{months:"\u05d9\u05e0\u05d5\u05d0\u05e8_\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05d9\u05dc_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8_\u05e1\u05e4\u05d8\u05de\u05d1\u05e8_\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8_\u05e0\u05d5\u05d1\u05de\u05d1\u05e8_\u05d3\u05e6\u05de\u05d1\u05e8".split("_"),monthsShort:"\u05d9\u05e0\u05d5\u05f3_\u05e4\u05d1\u05e8\u05f3_\u05de\u05e8\u05e5_\u05d0\u05e4\u05e8\u05f3_\u05de\u05d0\u05d9_\u05d9\u05d5\u05e0\u05d9_\u05d9\u05d5\u05dc\u05d9_\u05d0\u05d5\u05d2\u05f3_\u05e1\u05e4\u05d8\u05f3_\u05d0\u05d5\u05e7\u05f3_\u05e0\u05d5\u05d1\u05f3_\u05d3\u05e6\u05de\u05f3".split("_"),weekdays:"\u05e8\u05d0\u05e9\u05d5\u05df_\u05e9\u05e0\u05d9_\u05e9\u05dc\u05d9\u05e9\u05d9_\u05e8\u05d1\u05d9\u05e2\u05d9_\u05d7\u05de\u05d9\u05e9\u05d9_\u05e9\u05d9\u05e9\u05d9_\u05e9\u05d1\u05ea".split("_"),weekdaysShort:"\u05d0\u05f3_\u05d1\u05f3_\u05d2\u05f3_\u05d3\u05f3_\u05d4\u05f3_\u05d5\u05f3_\u05e9\u05f3".split("_"),weekdaysMin:"\u05d0_\u05d1_\u05d2_\u05d3_\u05d4_\u05d5_\u05e9".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [\u05d1]MMMM YYYY",LLL:"D [\u05d1]MMMM YYYY HH:mm",LLLL:"dddd, D [\u05d1]MMMM YYYY HH:mm",l:"D/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[\u05d4\u05d9\u05d5\u05dd \u05d1\u05be]LT",nextDay:"[\u05de\u05d7\u05e8 \u05d1\u05be]LT",nextWeek:"dddd [\u05d1\u05e9\u05e2\u05d4] LT",lastDay:"[\u05d0\u05ea\u05de\u05d5\u05dc \u05d1\u05be]LT",lastWeek:"[\u05d1\u05d9\u05d5\u05dd] dddd [\u05d4\u05d0\u05d7\u05e8\u05d5\u05df \u05d1\u05e9\u05e2\u05d4] LT",sameElse:"L"},relativeTime:{future:"\u05d1\u05e2\u05d5\u05d3 %s",past:"\u05dc\u05e4\u05e0\u05d9 %s",s:"\u05de\u05e1\u05e4\u05e8 \u05e9\u05e0\u05d9\u05d5\u05ea",ss:"%d \u05e9\u05e0\u05d9\u05d5\u05ea",m:"\u05d3\u05e7\u05d4",mm:"%d \u05d3\u05e7\u05d5\u05ea",h:"\u05e9\u05e2\u05d4",hh:function(e){return 2===e?"\u05e9\u05e2\u05ea\u05d9\u05d9\u05dd":e+" \u05e9\u05e2\u05d5\u05ea"},d:"\u05d9\u05d5\u05dd",dd:function(e){return 2===e?"\u05d9\u05d5\u05de\u05d9\u05d9\u05dd":e+" \u05d9\u05de\u05d9\u05dd"},M:"\u05d7\u05d5\u05d3\u05e9",MM:function(e){return 2===e?"\u05d7\u05d5\u05d3\u05e9\u05d9\u05d9\u05dd":e+" \u05d7\u05d5\u05d3\u05e9\u05d9\u05dd"},y:"\u05e9\u05e0\u05d4",yy:function(e){return 2===e?"\u05e9\u05e0\u05ea\u05d9\u05d9\u05dd":e%10==0&&10!==e?e+" \u05e9\u05e0\u05d4":e+" \u05e9\u05e0\u05d9\u05dd"}},meridiemParse:/\u05d0\u05d7\u05d4"\u05e6|\u05dc\u05e4\u05e0\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8|\u05d1\u05d1\u05d5\u05e7\u05e8|\u05d1\u05e2\u05e8\u05d1/i,isPM:function(e){return/^(\u05d0\u05d7\u05d4"\u05e6|\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd|\u05d1\u05e2\u05e8\u05d1)$/.test(e)},meridiem:function(e,a,t){return e<5?"\u05dc\u05e4\u05e0\u05d5\u05ea \u05d1\u05d5\u05e7\u05e8":e<10?"\u05d1\u05d1\u05d5\u05e7\u05e8":e<12?t?'\u05dc\u05e4\u05e0\u05d4"\u05e6':"\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":e<18?t?'\u05d0\u05d7\u05d4"\u05e6':"\u05d0\u05d7\u05e8\u05d9 \u05d4\u05e6\u05d4\u05e8\u05d9\u05d9\u05dd":"\u05d1\u05e2\u05e8\u05d1"}});var Ln={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},cn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("hi",{months:"\u091c\u0928\u0935\u0930\u0940_\u092b\u093c\u0930\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948\u0932_\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0938\u094d\u0924_\u0938\u093f\u0924\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u0942\u092c\u0930_\u0928\u0935\u092e\u094d\u092c\u0930_\u0926\u093f\u0938\u092e\u094d\u092c\u0930".split("_"),monthsShort:"\u091c\u0928._\u092b\u093c\u0930._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u0948._\u092e\u0908_\u091c\u0942\u0928_\u091c\u0941\u0932._\u0905\u0917._\u0938\u093f\u0924._\u0905\u0915\u094d\u091f\u0942._\u0928\u0935._\u0926\u093f\u0938.".split("_"),monthsParseExact:!0,weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0932\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0932_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u092c\u091c\u0947",LTS:"A h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A h:mm \u092c\u091c\u0947"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0915\u0932] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u0932] LT",lastWeek:"[\u092a\u093f\u091b\u0932\u0947] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u092e\u0947\u0902",past:"%s \u092a\u0939\u0932\u0947",s:"\u0915\u0941\u091b \u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0902\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u091f",mm:"%d \u092e\u093f\u0928\u091f",h:"\u090f\u0915 \u0918\u0902\u091f\u093e",hh:"%d \u0918\u0902\u091f\u0947",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u0940\u0928\u0947",MM:"%d \u092e\u0939\u0940\u0928\u0947",y:"\u090f\u0915 \u0935\u0930\u094d\u0937",yy:"%d \u0935\u0930\u094d\u0937"},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return cn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Ln[e]})},meridiemParse:/\u0930\u093e\u0924|\u0938\u0941\u092c\u0939|\u0926\u094b\u092a\u0939\u0930|\u0936\u093e\u092e/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924"===a?e<4?e:e+12:"\u0938\u0941\u092c\u0939"===a?e:"\u0926\u094b\u092a\u0939\u0930"===a?e>=10?e:e+12:"\u0936\u093e\u092e"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0930\u093e\u0924":e<10?"\u0938\u0941\u092c\u0939":e<17?"\u0926\u094b\u092a\u0939\u0930":e<20?"\u0936\u093e\u092e":"\u0930\u093e\u0924"},week:{dow:0,doy:6}}),e.defineLocale("hr",{months:{format:"sije\u010dnja_velja\u010de_o\u017eujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca".split("_"),standalone:"sije\u010danj_velja\u010da_o\u017eujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac".split("_")},monthsShort:"sij._velj._o\u017eu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010der u] LT",lastWeek:function(){switch(this.day()){case 0:case 3:return"[pro\u0161lu] dddd [u] LT";case 6:return"[pro\u0161le] [subote] [u] LT";case 1:case 2:case 4:case 5:return"[pro\u0161li] dddd [u] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"par sekundi",ss:oa,m:oa,mm:oa,h:oa,hh:oa,d:"dan",dd:oa,M:"mjesec",MM:oa,y:"godinu",yy:oa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var Yn="vas\xe1rnap h\xe9tf\u0151n kedden szerd\xe1n cs\xfct\xf6rt\xf6k\xf6n p\xe9nteken szombaton".split(" ");e.defineLocale("hu",{months:"janu\xe1r_febru\xe1r_m\xe1rcius_\xe1prilis_m\xe1jus_j\xfanius_j\xfalius_augusztus_szeptember_okt\xf3ber_november_december".split("_"),monthsShort:"jan_feb_m\xe1rc_\xe1pr_m\xe1j_j\xfan_j\xfal_aug_szept_okt_nov_dec".split("_"),weekdays:"vas\xe1rnap_h\xe9tf\u0151_kedd_szerda_cs\xfct\xf6rt\xf6k_p\xe9ntek_szombat".split("_"),weekdaysShort:"vas_h\xe9t_kedd_sze_cs\xfct_p\xe9n_szo".split("_"),weekdaysMin:"v_h_k_sze_cs_p_szo".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"YYYY.MM.DD.",LL:"YYYY. MMMM D.",LLL:"YYYY. MMMM D. H:mm",LLLL:"YYYY. MMMM D., dddd H:mm"},meridiemParse:/de|du/i,isPM:function(e){return"u"===e.charAt(1).toLowerCase()},meridiem:function(e,a,t){return e<12?!0===t?"de":"DE":!0===t?"du":"DU"},calendar:{sameDay:"[ma] LT[-kor]",nextDay:"[holnap] LT[-kor]",nextWeek:function(){return ua.call(this,!0)},lastDay:"[tegnap] LT[-kor]",lastWeek:function(){return ua.call(this,!1)},sameElse:"L"},relativeTime:{future:"%s m\xfalva",past:"%s",s:ma,ss:ma,m:ma,mm:ma,h:ma,hh:ma,d:ma,dd:ma,M:ma,MM:ma,y:ma,yy:ma},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("hy-am",{months:{format:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580\u056b_\u0583\u0565\u057f\u0580\u057e\u0561\u0580\u056b_\u0574\u0561\u0580\u057f\u056b_\u0561\u057a\u0580\u056b\u056c\u056b_\u0574\u0561\u0575\u056b\u057d\u056b_\u0570\u0578\u0582\u0576\u056b\u057d\u056b_\u0570\u0578\u0582\u056c\u056b\u057d\u056b_\u0585\u0563\u0578\u057d\u057f\u0578\u057d\u056b_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580\u056b_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580\u056b".split("_"),standalone:"\u0570\u0578\u0582\u0576\u057e\u0561\u0580_\u0583\u0565\u057f\u0580\u057e\u0561\u0580_\u0574\u0561\u0580\u057f_\u0561\u057a\u0580\u056b\u056c_\u0574\u0561\u0575\u056b\u057d_\u0570\u0578\u0582\u0576\u056b\u057d_\u0570\u0578\u0582\u056c\u056b\u057d_\u0585\u0563\u0578\u057d\u057f\u0578\u057d_\u057d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580_\u0570\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580_\u0576\u0578\u0575\u0565\u0574\u0562\u0565\u0580_\u0564\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580".split("_")},monthsShort:"\u0570\u0576\u057e_\u0583\u057f\u0580_\u0574\u0580\u057f_\u0561\u057a\u0580_\u0574\u0575\u057d_\u0570\u0576\u057d_\u0570\u056c\u057d_\u0585\u0563\u057d_\u057d\u057a\u057f_\u0570\u056f\u057f_\u0576\u0574\u0562_\u0564\u056f\u057f".split("_"),weekdays:"\u056f\u056b\u0580\u0561\u056f\u056b_\u0565\u0580\u056f\u0578\u0582\u0577\u0561\u0562\u0569\u056b_\u0565\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0579\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b_\u0570\u056b\u0576\u0563\u0577\u0561\u0562\u0569\u056b_\u0578\u0582\u0580\u0562\u0561\u0569_\u0577\u0561\u0562\u0561\u0569".split("_"),weekdaysShort:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),weekdaysMin:"\u056f\u0580\u056f_\u0565\u0580\u056f_\u0565\u0580\u0584_\u0579\u0580\u0584_\u0570\u0576\u0563_\u0578\u0582\u0580\u0562_\u0577\u0562\u0569".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0569.",LLL:"D MMMM YYYY \u0569., HH:mm",LLLL:"dddd, D MMMM YYYY \u0569., HH:mm"},calendar:{sameDay:"[\u0561\u0575\u057d\u0585\u0580] LT",nextDay:"[\u057e\u0561\u0572\u0568] LT",lastDay:"[\u0565\u0580\u0565\u056f] LT",nextWeek:function(){return"dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},lastWeek:function(){return"[\u0561\u0576\u0581\u0561\u056e] dddd [\u0585\u0580\u0568 \u056a\u0561\u0574\u0568] LT"},sameElse:"L"},relativeTime:{future:"%s \u0570\u0565\u057f\u0578",past:"%s \u0561\u057c\u0561\u057b",s:"\u0574\u056b \u0584\u0561\u0576\u056b \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",ss:"%d \u057e\u0561\u0575\u0580\u056f\u0575\u0561\u0576",m:"\u0580\u0578\u057a\u0565",mm:"%d \u0580\u0578\u057a\u0565",h:"\u056a\u0561\u0574",hh:"%d \u056a\u0561\u0574",d:"\u0585\u0580",dd:"%d \u0585\u0580",M:"\u0561\u0574\u056b\u057d",MM:"%d \u0561\u0574\u056b\u057d",y:"\u057f\u0561\u0580\u056b",yy:"%d \u057f\u0561\u0580\u056b"},meridiemParse:/\u0563\u056b\u0577\u0565\u0580\u057e\u0561|\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561|\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576/,isPM:function(e){return/^(\u0581\u0565\u0580\u0565\u056f\u057e\u0561|\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576)$/.test(e)},meridiem:function(e){return e<4?"\u0563\u056b\u0577\u0565\u0580\u057e\u0561":e<12?"\u0561\u057c\u0561\u057e\u0578\u057f\u057e\u0561":e<17?"\u0581\u0565\u0580\u0565\u056f\u057e\u0561":"\u0565\u0580\u0565\u056f\u0578\u0575\u0561\u0576"},dayOfMonthOrdinalParse:/\d{1,2}|\d{1,2}-(\u056b\u0576|\u0580\u0564)/,ordinal:function(e,a){switch(a){case"DDD":case"w":case"W":case"DDDo":return 1===e?e+"-\u056b\u0576":e+"-\u0580\u0564";default:return e}},week:{dow:1,doy:7}}),e.defineLocale("id",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_November_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nov_Des".split("_"),weekdays:"Minggu_Senin_Selasa_Rabu_Kamis_Jumat_Sabtu".split("_"),weekdaysShort:"Min_Sen_Sel_Rab_Kam_Jum_Sab".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|siang|sore|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"siang"===a?e>=11?e:e+12:"sore"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"siang":e<19?"sore":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Besok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kemarin pukul] LT",lastWeek:"dddd [lalu pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lalu",s:"beberapa detik",ss:"%d detik",m:"semenit",mm:"%d menit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("is",{months:"jan\xfaar_febr\xfaar_mars_apr\xedl_ma\xed_j\xfan\xed_j\xfal\xed_\xe1g\xfast_september_okt\xf3ber_n\xf3vember_desember".split("_"),monthsShort:"jan_feb_mar_apr_ma\xed_j\xfan_j\xfal_\xe1g\xfa_sep_okt_n\xf3v_des".split("_"),weekdays:"sunnudagur_m\xe1nudagur_\xferi\xf0judagur_mi\xf0vikudagur_fimmtudagur_f\xf6studagur_laugardagur".split("_"),weekdaysShort:"sun_m\xe1n_\xferi_mi\xf0_fim_f\xf6s_lau".split("_"),weekdaysMin:"Su_M\xe1_\xder_Mi_Fi_F\xf6_La".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd, D. MMMM YYYY [kl.] H:mm"},calendar:{sameDay:"[\xed dag kl.] LT",nextDay:"[\xe1 morgun kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[\xed g\xe6r kl.] LT",lastWeek:"[s\xed\xf0asta] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"eftir %s",past:"fyrir %s s\xed\xf0an",s:Ma,ss:Ma,m:Ma,mm:Ma,h:"klukkustund",hh:Ma,d:Ma,dd:Ma,M:Ma,MM:Ma,y:Ma,yy:Ma},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("it",{months:"gennaio_febbraio_marzo_aprile_maggio_giugno_luglio_agosto_settembre_ottobre_novembre_dicembre".split("_"),monthsShort:"gen_feb_mar_apr_mag_giu_lug_ago_set_ott_nov_dic".split("_"),weekdays:"domenica_luned\xec_marted\xec_mercoled\xec_gioved\xec_venerd\xec_sabato".split("_"),weekdaysShort:"dom_lun_mar_mer_gio_ven_sab".split("_"),weekdaysMin:"do_lu_ma_me_gi_ve_sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[Oggi alle] LT",nextDay:"[Domani alle] LT",nextWeek:"dddd [alle] LT",lastDay:"[Ieri alle] LT",lastWeek:function(){switch(this.day()){case 0:return"[la scorsa] dddd [alle] LT";default:return"[lo scorso] dddd [alle] LT"}},sameElse:"L"},relativeTime:{future:function(e){return(/^[0-9].+$/.test(e)?"tra":"in")+" "+e},past:"%s fa",s:"alcuni secondi",ss:"%d secondi",m:"un minuto",mm:"%d minuti",h:"un'ora",hh:"%d ore",d:"un giorno",dd:"%d giorni",M:"un mese",MM:"%d mesi",y:"un anno",yy:"%d anni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("ja",{months:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u65e5\u66dc\u65e5_\u6708\u66dc\u65e5_\u706b\u66dc\u65e5_\u6c34\u66dc\u65e5_\u6728\u66dc\u65e5_\u91d1\u66dc\u65e5_\u571f\u66dc\u65e5".split("_"),weekdaysShort:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),weekdaysMin:"\u65e5_\u6708_\u706b_\u6c34_\u6728_\u91d1_\u571f".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm dddd",l:"YYYY/MM/DD",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5 HH:mm dddd"},meridiemParse:/\u5348\u524d|\u5348\u5f8c/i,isPM:function(e){return"\u5348\u5f8c"===e},meridiem:function(e,a,t){return e<12?"\u5348\u524d":"\u5348\u5f8c"},calendar:{sameDay:"[\u4eca\u65e5] LT",nextDay:"[\u660e\u65e5] LT",nextWeek:"[\u6765\u9031]dddd LT",lastDay:"[\u6628\u65e5] LT",lastWeek:"[\u524d\u9031]dddd LT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}\u65e5/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";default:return e}},relativeTime:{future:"%s\u5f8c",past:"%s\u524d",s:"\u6570\u79d2",ss:"%d\u79d2",m:"1\u5206",mm:"%d\u5206",h:"1\u6642\u9593",hh:"%d\u6642\u9593",d:"1\u65e5",dd:"%d\u65e5",M:"1\u30f6\u6708",MM:"%d\u30f6\u6708",y:"1\u5e74",yy:"%d\u5e74"}}),e.defineLocale("jv",{months:"Januari_Februari_Maret_April_Mei_Juni_Juli_Agustus_September_Oktober_Nopember_Desember".split("_"),monthsShort:"Jan_Feb_Mar_Apr_Mei_Jun_Jul_Ags_Sep_Okt_Nop_Des".split("_"),weekdays:"Minggu_Senen_Seloso_Rebu_Kemis_Jemuwah_Septu".split("_"),weekdaysShort:"Min_Sen_Sel_Reb_Kem_Jem_Sep".split("_"),weekdaysMin:"Mg_Sn_Sl_Rb_Km_Jm_Sp".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/enjing|siyang|sonten|ndalu/,meridiemHour:function(e,a){return 12===e&&(e=0),"enjing"===a?e:"siyang"===a?e>=11?e:e+12:"sonten"===a||"ndalu"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"enjing":e<15?"siyang":e<19?"sonten":"ndalu"},calendar:{sameDay:"[Dinten puniko pukul] LT",nextDay:"[Mbenjang pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kala wingi pukul] LT",lastWeek:"dddd [kepengker pukul] LT",sameElse:"L"},relativeTime:{future:"wonten ing %s",past:"%s ingkang kepengker",s:"sawetawis detik",ss:"%d detik",m:"setunggal menit",mm:"%d menit",h:"setunggal jam",hh:"%d jam",d:"sedinten",dd:"%d dinten",M:"sewulan",MM:"%d wulan",y:"setaun",yy:"%d taun"},week:{dow:1,doy:7}}),e.defineLocale("ka",{months:{standalone:"\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10d8_\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10d8_\u10db\u10d0\u10e0\u10e2\u10d8_\u10d0\u10de\u10e0\u10d8\u10da\u10d8_\u10db\u10d0\u10d8\u10e1\u10d8_\u10d8\u10d5\u10dc\u10d8\u10e1\u10d8_\u10d8\u10d5\u10da\u10d8\u10e1\u10d8_\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10dd_\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10d8_\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10d8_\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10d8".split("_"),format:"\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10e1_\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10e1_\u10db\u10d0\u10e0\u10e2\u10e1_\u10d0\u10de\u10e0\u10d8\u10da\u10d8\u10e1_\u10db\u10d0\u10d8\u10e1\u10e1_\u10d8\u10d5\u10dc\u10d8\u10e1\u10e1_\u10d8\u10d5\u10da\u10d8\u10e1\u10e1_\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10e1_\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10e1_\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10e1_\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10e1_\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10e1".split("_")},monthsShort:"\u10d8\u10d0\u10dc_\u10d7\u10d4\u10d1_\u10db\u10d0\u10e0_\u10d0\u10de\u10e0_\u10db\u10d0\u10d8_\u10d8\u10d5\u10dc_\u10d8\u10d5\u10da_\u10d0\u10d2\u10d5_\u10e1\u10d4\u10e5_\u10dd\u10e5\u10e2_\u10dc\u10dd\u10d4_\u10d3\u10d4\u10d9".split("_"),weekdays:{standalone:"\u10d9\u10d5\u10d8\u10e0\u10d0_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10d8_\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8".split("_"),format:"\u10d9\u10d5\u10d8\u10e0\u10d0\u10e1_\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1_\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10e1_\u10e8\u10d0\u10d1\u10d0\u10d7\u10e1".split("_"),isFormat:/(\u10ec\u10d8\u10dc\u10d0|\u10e8\u10d4\u10db\u10d3\u10d4\u10d2)/},weekdaysShort:"\u10d9\u10d5\u10d8_\u10dd\u10e0\u10e8_\u10e1\u10d0\u10db_\u10dd\u10d7\u10ee_\u10ee\u10e3\u10d7_\u10de\u10d0\u10e0_\u10e8\u10d0\u10d1".split("_"),weekdaysMin:"\u10d9\u10d5_\u10dd\u10e0_\u10e1\u10d0_\u10dd\u10d7_\u10ee\u10e3_\u10de\u10d0_\u10e8\u10d0".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[\u10d3\u10e6\u10d4\u10e1] LT[-\u10d6\u10d4]",nextDay:"[\u10ee\u10d5\u10d0\u10da] LT[-\u10d6\u10d4]",lastDay:"[\u10d2\u10e3\u10e8\u10d8\u10dc] LT[-\u10d6\u10d4]",nextWeek:"[\u10e8\u10d4\u10db\u10d3\u10d4\u10d2] dddd LT[-\u10d6\u10d4]",lastWeek:"[\u10ec\u10d8\u10dc\u10d0] dddd LT-\u10d6\u10d4",sameElse:"L"},relativeTime:{future:function(e){return/(\u10ec\u10d0\u10db\u10d8|\u10ec\u10e3\u10d7\u10d8|\u10e1\u10d0\u10d0\u10d7\u10d8|\u10ec\u10d4\u10da\u10d8)/.test(e)?e.replace(/\u10d8$/,"\u10e8\u10d8"):e+"\u10e8\u10d8"},past:function(e){return/(\u10ec\u10d0\u10db\u10d8|\u10ec\u10e3\u10d7\u10d8|\u10e1\u10d0\u10d0\u10d7\u10d8|\u10d3\u10e6\u10d4|\u10d7\u10d5\u10d4)/.test(e)?e.replace(/(\u10d8|\u10d4)$/,"\u10d8\u10e1 \u10e3\u10d9\u10d0\u10dc"):/\u10ec\u10d4\u10da\u10d8/.test(e)?e.replace(/\u10ec\u10d4\u10da\u10d8$/,"\u10ec\u10da\u10d8\u10e1 \u10e3\u10d9\u10d0\u10dc"):void 0},s:"\u10e0\u10d0\u10db\u10d3\u10d4\u10dc\u10d8\u10db\u10d4 \u10ec\u10d0\u10db\u10d8",ss:"%d \u10ec\u10d0\u10db\u10d8",m:"\u10ec\u10e3\u10d7\u10d8",mm:"%d \u10ec\u10e3\u10d7\u10d8",h:"\u10e1\u10d0\u10d0\u10d7\u10d8",hh:"%d \u10e1\u10d0\u10d0\u10d7\u10d8",d:"\u10d3\u10e6\u10d4",dd:"%d \u10d3\u10e6\u10d4",M:"\u10d7\u10d5\u10d4",MM:"%d \u10d7\u10d5\u10d4",y:"\u10ec\u10d4\u10da\u10d8",yy:"%d \u10ec\u10d4\u10da\u10d8"},dayOfMonthOrdinalParse:/0|1-\u10da\u10d8|\u10db\u10d4-\d{1,2}|\d{1,2}-\u10d4/,ordinal:function(e){return 0===e?e:1===e?e+"-\u10da\u10d8":e<20||e<=100&&e%20==0||e%100==0?"\u10db\u10d4-"+e:e+"-\u10d4"},week:{dow:1,doy:7}});var yn={0:"-\u0448\u0456",1:"-\u0448\u0456",2:"-\u0448\u0456",3:"-\u0448\u0456",4:"-\u0448\u0456",5:"-\u0448\u0456",6:"-\u0448\u044b",7:"-\u0448\u0456",8:"-\u0448\u0456",9:"-\u0448\u044b",10:"-\u0448\u044b",20:"-\u0448\u044b",30:"-\u0448\u044b",40:"-\u0448\u044b",50:"-\u0448\u0456",60:"-\u0448\u044b",70:"-\u0448\u0456",80:"-\u0448\u0456",90:"-\u0448\u044b",100:"-\u0448\u0456"};e.defineLocale("kk",{months:"\u049b\u0430\u04a3\u0442\u0430\u0440_\u0430\u049b\u043f\u0430\u043d_\u043d\u0430\u0443\u0440\u044b\u0437_\u0441\u04d9\u0443\u0456\u0440_\u043c\u0430\u043c\u044b\u0440_\u043c\u0430\u0443\u0441\u044b\u043c_\u0448\u0456\u043b\u0434\u0435_\u0442\u0430\u043c\u044b\u0437_\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a_\u049b\u0430\u0437\u0430\u043d_\u049b\u0430\u0440\u0430\u0448\u0430_\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d".split("_"),monthsShort:"\u049b\u0430\u04a3_\u0430\u049b\u043f_\u043d\u0430\u0443_\u0441\u04d9\u0443_\u043c\u0430\u043c_\u043c\u0430\u0443_\u0448\u0456\u043b_\u0442\u0430\u043c_\u049b\u044b\u0440_\u049b\u0430\u0437_\u049b\u0430\u0440_\u0436\u0435\u043b".split("_"),weekdays:"\u0436\u0435\u043a\u0441\u0435\u043d\u0431\u0456_\u0434\u04af\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0441\u04d9\u0440\u0441\u0435\u043d\u0431\u0456_\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456_\u0436\u04b1\u043c\u0430_\u0441\u0435\u043d\u0431\u0456".split("_"),weekdaysShort:"\u0436\u0435\u043a_\u0434\u04af\u0439_\u0441\u0435\u0439_\u0441\u04d9\u0440_\u0431\u0435\u0439_\u0436\u04b1\u043c_\u0441\u0435\u043d".split("_"),weekdaysMin:"\u0436\u043a_\u0434\u0439_\u0441\u0439_\u0441\u0440_\u0431\u0439_\u0436\u043c_\u0441\u043d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u0456\u043d \u0441\u0430\u0493\u0430\u0442] LT",nextDay:"[\u0415\u0440\u0442\u0435\u04a3 \u0441\u0430\u0493\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0493\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0448\u0435 \u0441\u0430\u0493\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u0435\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u04a3] dddd [\u0441\u0430\u0493\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0456\u0448\u0456\u043d\u0434\u0435",past:"%s \u0431\u04b1\u0440\u044b\u043d",s:"\u0431\u0456\u0440\u043d\u0435\u0448\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0456\u0440 \u043c\u0438\u043d\u0443\u0442",mm:"%d \u043c\u0438\u043d\u0443\u0442",h:"\u0431\u0456\u0440 \u0441\u0430\u0493\u0430\u0442",hh:"%d \u0441\u0430\u0493\u0430\u0442",d:"\u0431\u0456\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0456\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0456\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0448\u0456|\u0448\u044b)/,ordinal:function(e){return e+(yn[e]||yn[e%10]||yn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("km",{months:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),monthsShort:"\u1798\u1780\u179a\u17b6_\u1780\u17bb\u1798\u17d2\u1797\u17c8_\u1798\u17b8\u1793\u17b6_\u1798\u17c1\u179f\u17b6_\u17a7\u179f\u1797\u17b6_\u1798\u17b7\u1790\u17bb\u1793\u17b6_\u1780\u1780\u17d2\u1780\u178a\u17b6_\u179f\u17b8\u17a0\u17b6_\u1780\u1789\u17d2\u1789\u17b6_\u178f\u17bb\u179b\u17b6_\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6_\u1792\u17d2\u1793\u17bc".split("_"),weekdays:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),weekdaysShort:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),weekdaysMin:"\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799_\u1785\u17d0\u1793\u17d2\u1791_\u17a2\u1784\u17d2\u1782\u17b6\u179a_\u1796\u17bb\u1792_\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd_\u179f\u17bb\u1780\u17d2\u179a_\u179f\u17c5\u179a\u17cd".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u1790\u17d2\u1784\u17c3\u1793\u17c1\u17c7 \u1798\u17c9\u17c4\u1784] LT",nextDay:"[\u179f\u17d2\u17a2\u17c2\u1780 \u1798\u17c9\u17c4\u1784] LT",nextWeek:"dddd [\u1798\u17c9\u17c4\u1784] LT",lastDay:"[\u1798\u17d2\u179f\u17b7\u179b\u1798\u17b7\u1789 \u1798\u17c9\u17c4\u1784] LT",lastWeek:"dddd [\u179f\u1794\u17d2\u178f\u17b6\u17a0\u17cd\u1798\u17bb\u1793] [\u1798\u17c9\u17c4\u1784] LT",sameElse:"L"},relativeTime:{future:"%s\u1791\u17c0\u178f",past:"%s\u1798\u17bb\u1793",s:"\u1794\u17c9\u17bb\u1793\u17d2\u1798\u17b6\u1793\u179c\u17b7\u1793\u17b6\u1791\u17b8",ss:"%d \u179c\u17b7\u1793\u17b6\u1791\u17b8",m:"\u1798\u17bd\u1799\u1793\u17b6\u1791\u17b8",mm:"%d \u1793\u17b6\u1791\u17b8",h:"\u1798\u17bd\u1799\u1798\u17c9\u17c4\u1784",hh:"%d \u1798\u17c9\u17c4\u1784",d:"\u1798\u17bd\u1799\u1790\u17d2\u1784\u17c3",dd:"%d \u1790\u17d2\u1784\u17c3",M:"\u1798\u17bd\u1799\u1781\u17c2",MM:"%d \u1781\u17c2",y:"\u1798\u17bd\u1799\u1786\u17d2\u1793\u17b6\u17c6",yy:"%d \u1786\u17d2\u1793\u17b6\u17c6"},week:{dow:1,doy:4}});var fn={1:"\u0ce7",2:"\u0ce8",3:"\u0ce9",4:"\u0cea",5:"\u0ceb",6:"\u0cec",7:"\u0ced",8:"\u0cee",9:"\u0cef",0:"\u0ce6"},kn={"\u0ce7":"1","\u0ce8":"2","\u0ce9":"3","\u0cea":"4","\u0ceb":"5","\u0cec":"6","\u0ced":"7","\u0cee":"8","\u0cef":"9","\u0ce6":"0"};e.defineLocale("kn",{months:"\u0c9c\u0ca8\u0cb5\u0cb0\u0cbf_\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cbf_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5\u0cac\u0cb0\u0ccd_\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd".split("_"),monthsShort:"\u0c9c\u0ca8_\u0cab\u0cc6\u0cac\u0ccd\u0cb0_\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd_\u0c8f\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd_\u0cae\u0cc6\u0cd5_\u0c9c\u0cc2\u0ca8\u0ccd_\u0c9c\u0cc1\u0cb2\u0cc6\u0cd6_\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd_\u0cb8\u0cc6\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac_\u0c85\u0c95\u0ccd\u0c9f\u0cc6\u0cc2\u0cd5\u0cac_\u0ca8\u0cb5\u0cc6\u0c82\u0cac_\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac".split("_"),monthsParseExact:!0,weekdays:"\u0cad\u0cbe\u0ca8\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae\u0cb5\u0cbe\u0cb0_\u0cae\u0c82\u0c97\u0cb3\u0cb5\u0cbe\u0cb0_\u0cac\u0cc1\u0ca7\u0cb5\u0cbe\u0cb0_\u0c97\u0cc1\u0cb0\u0cc1\u0cb5\u0cbe\u0cb0_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0\u0cb5\u0cbe\u0cb0_\u0cb6\u0ca8\u0cbf\u0cb5\u0cbe\u0cb0".split("_"),weekdaysShort:"\u0cad\u0cbe\u0ca8\u0cc1_\u0cb8\u0cc6\u0cc2\u0cd5\u0cae_\u0cae\u0c82\u0c97\u0cb3_\u0cac\u0cc1\u0ca7_\u0c97\u0cc1\u0cb0\u0cc1_\u0cb6\u0cc1\u0c95\u0ccd\u0cb0_\u0cb6\u0ca8\u0cbf".split("_"),weekdaysMin:"\u0cad\u0cbe_\u0cb8\u0cc6\u0cc2\u0cd5_\u0cae\u0c82_\u0cac\u0cc1_\u0c97\u0cc1_\u0cb6\u0cc1_\u0cb6".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c87\u0c82\u0ca6\u0cc1] LT",nextDay:"[\u0ca8\u0cbe\u0cb3\u0cc6] LT",nextWeek:"dddd, LT",lastDay:"[\u0ca8\u0cbf\u0ca8\u0ccd\u0ca8\u0cc6] LT",lastWeek:"[\u0c95\u0cc6\u0cc2\u0ca8\u0cc6\u0caf] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0ca8\u0c82\u0ca4\u0cb0",past:"%s \u0cb9\u0cbf\u0c82\u0ca6\u0cc6",s:"\u0c95\u0cc6\u0cb2\u0cb5\u0cc1 \u0c95\u0ccd\u0cb7\u0ca3\u0c97\u0cb3\u0cc1",ss:"%d \u0cb8\u0cc6\u0c95\u0cc6\u0c82\u0ca1\u0cc1\u0c97\u0cb3\u0cc1",m:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",mm:"%d \u0ca8\u0cbf\u0cae\u0cbf\u0cb7",h:"\u0c92\u0c82\u0ca6\u0cc1 \u0c97\u0c82\u0c9f\u0cc6",hh:"%d \u0c97\u0c82\u0c9f\u0cc6",d:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca6\u0cbf\u0ca8",dd:"%d \u0ca6\u0cbf\u0ca8",M:"\u0c92\u0c82\u0ca6\u0cc1 \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",MM:"%d \u0ca4\u0cbf\u0c82\u0c97\u0cb3\u0cc1",y:"\u0c92\u0c82\u0ca6\u0cc1 \u0cb5\u0cb0\u0ccd\u0cb7",yy:"%d \u0cb5\u0cb0\u0ccd\u0cb7"},preparse:function(e){return e.replace(/[\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0ce6]/g,function(e){return kn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return fn[e]})},meridiemParse:/\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf|\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6|\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8|\u0cb8\u0c82\u0c9c\u0cc6/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"===a?e<4?e:e+12:"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6"===a?e:"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8"===a?e>=10?e:e+12:"\u0cb8\u0c82\u0c9c\u0cc6"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf":e<10?"\u0cac\u0cc6\u0cb3\u0cbf\u0c97\u0ccd\u0c97\u0cc6":e<17?"\u0cae\u0ca7\u0ccd\u0caf\u0cbe\u0cb9\u0ccd\u0ca8":e<20?"\u0cb8\u0c82\u0c9c\u0cc6":"\u0cb0\u0cbe\u0ca4\u0ccd\u0cb0\u0cbf"},dayOfMonthOrdinalParse:/\d{1,2}(\u0ca8\u0cc6\u0cd5)/,ordinal:function(e){return e+"\u0ca8\u0cc6\u0cd5"},week:{dow:0,doy:6}}),e.defineLocale("ko",{months:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),monthsShort:"1\uc6d4_2\uc6d4_3\uc6d4_4\uc6d4_5\uc6d4_6\uc6d4_7\uc6d4_8\uc6d4_9\uc6d4_10\uc6d4_11\uc6d4_12\uc6d4".split("_"),weekdays:"\uc77c\uc694\uc77c_\uc6d4\uc694\uc77c_\ud654\uc694\uc77c_\uc218\uc694\uc77c_\ubaa9\uc694\uc77c_\uae08\uc694\uc77c_\ud1a0\uc694\uc77c".split("_"),weekdaysShort:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),weekdaysMin:"\uc77c_\uc6d4_\ud654_\uc218_\ubaa9_\uae08_\ud1a0".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"YYYY.MM.DD",LL:"YYYY\ub144 MMMM D\uc77c",LLL:"YYYY\ub144 MMMM D\uc77c A h:mm",LLLL:"YYYY\ub144 MMMM D\uc77c dddd A h:mm",l:"YYYY.MM.DD",ll:"YYYY\ub144 MMMM D\uc77c",lll:"YYYY\ub144 MMMM D\uc77c A h:mm",llll:"YYYY\ub144 MMMM D\uc77c dddd A h:mm"},calendar:{sameDay:"\uc624\ub298 LT",nextDay:"\ub0b4\uc77c LT",nextWeek:"dddd LT",lastDay:"\uc5b4\uc81c LT",lastWeek:"\uc9c0\ub09c\uc8fc dddd LT",sameElse:"L"},relativeTime:{future:"%s \ud6c4",past:"%s \uc804",s:"\uba87 \ucd08",ss:"%d\ucd08",m:"1\ubd84",mm:"%d\ubd84",h:"\ud55c \uc2dc\uac04",hh:"%d\uc2dc\uac04",d:"\ud558\ub8e8",dd:"%d\uc77c",M:"\ud55c \ub2ec",MM:"%d\ub2ec",y:"\uc77c \ub144",yy:"%d\ub144"},dayOfMonthOrdinalParse:/\d{1,2}(\uc77c|\uc6d4|\uc8fc)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\uc77c";case"M":return e+"\uc6d4";case"w":case"W":return e+"\uc8fc";default:return e}},meridiemParse:/\uc624\uc804|\uc624\ud6c4/,isPM:function(e){return"\uc624\ud6c4"===e},meridiem:function(e,a,t){return e<12?"\uc624\uc804":"\uc624\ud6c4"}});var pn={0:"-\u0447\u04af",1:"-\u0447\u0438",2:"-\u0447\u0438",3:"-\u0447\u04af",4:"-\u0447\u04af",5:"-\u0447\u0438",6:"-\u0447\u044b",7:"-\u0447\u0438",8:"-\u0447\u0438",9:"-\u0447\u0443",10:"-\u0447\u0443",20:"-\u0447\u044b",30:"-\u0447\u0443",40:"-\u0447\u044b",50:"-\u0447\u04af",60:"-\u0447\u044b",70:"-\u0447\u0438",80:"-\u0447\u0438",90:"-\u0447\u0443",100:"-\u0447\u04af"};e.defineLocale("ky",{months:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u0416\u0435\u043a\u0448\u0435\u043c\u0431\u0438_\u0414\u04af\u0439\u0448\u04e9\u043c\u0431\u04af_\u0428\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0428\u0430\u0440\u0448\u0435\u043c\u0431\u0438_\u0411\u0435\u0439\u0448\u0435\u043c\u0431\u0438_\u0416\u0443\u043c\u0430_\u0418\u0448\u0435\u043c\u0431\u0438".split("_"),weekdaysShort:"\u0416\u0435\u043a_\u0414\u04af\u0439_\u0428\u0435\u0439_\u0428\u0430\u0440_\u0411\u0435\u0439_\u0416\u0443\u043c_\u0418\u0448\u0435".split("_"),weekdaysMin:"\u0416\u043a_\u0414\u0439_\u0428\u0439_\u0428\u0440_\u0411\u0439_\u0416\u043c_\u0418\u0448".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u0411\u04af\u0433\u04af\u043d \u0441\u0430\u0430\u0442] LT",nextDay:"[\u042d\u0440\u0442\u0435\u04a3 \u0441\u0430\u0430\u0442] LT",nextWeek:"dddd [\u0441\u0430\u0430\u0442] LT",lastDay:"[\u041a\u0435\u0447\u0435 \u0441\u0430\u0430\u0442] LT",lastWeek:"[\u04e8\u0442\u043a\u0435\u043d \u0430\u043f\u0442\u0430\u043d\u044b\u043d] dddd [\u043a\u04af\u043d\u04af] [\u0441\u0430\u0430\u0442] LT",sameElse:"L"},relativeTime:{future:"%s \u0438\u0447\u0438\u043d\u0434\u0435",past:"%s \u043c\u0443\u0440\u0443\u043d",s:"\u0431\u0438\u0440\u043d\u0435\u0447\u0435 \u0441\u0435\u043a\u0443\u043d\u0434",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434",m:"\u0431\u0438\u0440 \u043c\u04af\u043d\u04e9\u0442",mm:"%d \u043c\u04af\u043d\u04e9\u0442",h:"\u0431\u0438\u0440 \u0441\u0430\u0430\u0442",hh:"%d \u0441\u0430\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u04af\u043d",dd:"%d \u043a\u04af\u043d",M:"\u0431\u0438\u0440 \u0430\u0439",MM:"%d \u0430\u0439",y:"\u0431\u0438\u0440 \u0436\u044b\u043b",yy:"%d \u0436\u044b\u043b"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0447\u0438|\u0447\u044b|\u0447\u04af|\u0447\u0443)/,ordinal:function(e){return e+(pn[e]||pn[e%10]||pn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("lb",{months:"Januar_Februar_M\xe4erz_Abr\xebll_Mee_Juni_Juli_August_September_Oktober_November_Dezember".split("_"),monthsShort:"Jan._Febr._Mrz._Abr._Mee_Jun._Jul._Aug._Sept._Okt._Nov._Dez.".split("_"),monthsParseExact:!0,weekdays:"Sonndeg_M\xe9indeg_D\xebnschdeg_M\xebttwoch_Donneschdeg_Freideg_Samschdeg".split("_"),weekdaysShort:"So._M\xe9._D\xeb._M\xeb._Do._Fr._Sa.".split("_"),weekdaysMin:"So_M\xe9_D\xeb_M\xeb_Do_Fr_Sa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm [Auer]",LTS:"H:mm:ss [Auer]",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm [Auer]",LLLL:"dddd, D. MMMM YYYY H:mm [Auer]"},calendar:{sameDay:"[Haut um] LT",sameElse:"L",nextDay:"[Muer um] LT",nextWeek:"dddd [um] LT",lastDay:"[G\xebschter um] LT",lastWeek:function(){switch(this.day()){case 2:case 4:return"[Leschten] dddd [um] LT";default:return"[Leschte] dddd [um] LT"}}},relativeTime:{future:function(e){return La(e.substr(0,e.indexOf(" ")))?"a "+e:"an "+e},past:function(e){return La(e.substr(0,e.indexOf(" ")))?"viru "+e:"virun "+e},s:"e puer Sekonnen",ss:"%d Sekonnen",m:ha,mm:"%d Minutten",h:ha,hh:"%d Stonnen",d:ha,dd:"%d Deeg",M:ha,MM:"%d M\xe9int",y:ha,yy:"%d Joer"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("lo",{months:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),monthsShort:"\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99_\u0e81\u0eb8\u0ea1\u0e9e\u0eb2_\u0ea1\u0eb5\u0e99\u0eb2_\u0ec0\u0ea1\u0eaa\u0eb2_\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2_\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2_\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94_\u0eaa\u0eb4\u0e87\u0eab\u0eb2_\u0e81\u0eb1\u0e99\u0e8d\u0eb2_\u0e95\u0eb8\u0ea5\u0eb2_\u0e9e\u0eb0\u0e88\u0eb4\u0e81_\u0e97\u0eb1\u0e99\u0ea7\u0eb2".split("_"),weekdays:"\u0ead\u0eb2\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysShort:"\u0e97\u0eb4\u0e94_\u0e88\u0eb1\u0e99_\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99_\u0e9e\u0eb8\u0e94_\u0e9e\u0eb0\u0eab\u0eb1\u0e94_\u0eaa\u0eb8\u0e81_\u0ec0\u0eaa\u0ebb\u0eb2".split("_"),weekdaysMin:"\u0e97_\u0e88_\u0ead\u0e84_\u0e9e_\u0e9e\u0eab_\u0eaa\u0e81_\u0eaa".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"\u0ea7\u0eb1\u0e99dddd D MMMM YYYY HH:mm"},meridiemParse:/\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2|\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87/,isPM:function(e){return"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"===e},meridiem:function(e,a,t){return e<12?"\u0e95\u0ead\u0e99\u0ec0\u0e8a\u0ebb\u0ec9\u0eb2":"\u0e95\u0ead\u0e99\u0ec1\u0ea5\u0e87"},calendar:{sameDay:"[\u0ea1\u0eb7\u0ec9\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextDay:"[\u0ea1\u0eb7\u0ec9\u0ead\u0eb7\u0ec8\u0e99\u0ec0\u0ea7\u0ea5\u0eb2] LT",nextWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0edc\u0ec9\u0eb2\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastDay:"[\u0ea1\u0eb7\u0ec9\u0ea7\u0eb2\u0e99\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",lastWeek:"[\u0ea7\u0eb1\u0e99]dddd[\u0ec1\u0ea5\u0ec9\u0ea7\u0e99\u0eb5\u0ec9\u0ec0\u0ea7\u0ea5\u0eb2] LT",sameElse:"L"},relativeTime:{future:"\u0ead\u0eb5\u0e81 %s",past:"%s\u0e9c\u0ec8\u0eb2\u0e99\u0ea1\u0eb2",s:"\u0e9a\u0ecd\u0ec8\u0ec0\u0e97\u0ebb\u0ec8\u0eb2\u0ec3\u0e94\u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",ss:"%d \u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5",m:"1 \u0e99\u0eb2\u0e97\u0eb5",mm:"%d \u0e99\u0eb2\u0e97\u0eb5",h:"1 \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",hh:"%d \u0e8a\u0ebb\u0ec8\u0ea7\u0ec2\u0ea1\u0e87",d:"1 \u0ea1\u0eb7\u0ec9",dd:"%d \u0ea1\u0eb7\u0ec9",M:"1 \u0ec0\u0e94\u0eb7\u0ead\u0e99",MM:"%d \u0ec0\u0e94\u0eb7\u0ead\u0e99",y:"1 \u0e9b\u0eb5",yy:"%d \u0e9b\u0eb5"},dayOfMonthOrdinalParse:/(\u0e97\u0eb5\u0ec8)\d{1,2}/,ordinal:function(e){return"\u0e97\u0eb5\u0ec8"+e}});var Dn={ss:"sekund\u0117_sekund\u017ei\u0173_sekundes",m:"minut\u0117_minut\u0117s_minut\u0119",mm:"minut\u0117s_minu\u010di\u0173_minutes",h:"valanda_valandos_valand\u0105",hh:"valandos_valand\u0173_valandas",d:"diena_dienos_dien\u0105",dd:"dienos_dien\u0173_dienas",M:"m\u0117nuo_m\u0117nesio_m\u0117nes\u012f",MM:"m\u0117nesiai_m\u0117nesi\u0173_m\u0117nesius",y:"metai_met\u0173_metus",yy:"metai_met\u0173_metus"};e.defineLocale("lt",{months:{format:"sausio_vasario_kovo_baland\u017eio_gegu\u017e\u0117s_bir\u017eelio_liepos_rugpj\u016b\u010dio_rugs\u0117jo_spalio_lapkri\u010dio_gruod\u017eio".split("_"),standalone:"sausis_vasaris_kovas_balandis_gegu\u017e\u0117_bir\u017eelis_liepa_rugpj\u016btis_rugs\u0117jis_spalis_lapkritis_gruodis".split("_"),isFormat:/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?|MMMM?(\[[^\[\]]*\]|\s)+D[oD]?/},monthsShort:"sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd".split("_"),weekdays:{format:"sekmadien\u012f_pirmadien\u012f_antradien\u012f_tre\u010diadien\u012f_ketvirtadien\u012f_penktadien\u012f_\u0161e\u0161tadien\u012f".split("_"),standalone:"sekmadienis_pirmadienis_antradienis_tre\u010diadienis_ketvirtadienis_penktadienis_\u0161e\u0161tadienis".split("_"),isFormat:/dddd HH:mm/},weekdaysShort:"Sek_Pir_Ant_Tre_Ket_Pen_\u0160e\u0161".split("_"),weekdaysMin:"S_P_A_T_K_Pn_\u0160".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"YYYY [m.] MMMM D [d.]",LLL:"YYYY [m.] MMMM D [d.], HH:mm [val.]",LLLL:"YYYY [m.] MMMM D [d.], dddd, HH:mm [val.]",l:"YYYY-MM-DD",ll:"YYYY [m.] MMMM D [d.]",lll:"YYYY [m.] MMMM D [d.], HH:mm [val.]",llll:"YYYY [m.] MMMM D [d.], ddd, HH:mm [val.]"},calendar:{sameDay:"[\u0160iandien] LT",nextDay:"[Rytoj] LT",nextWeek:"dddd LT",lastDay:"[Vakar] LT",lastWeek:"[Pra\u0117jus\u012f] dddd LT",sameElse:"L"},relativeTime:{future:"po %s",past:"prie\u0161 %s",s:function(e,a,t,s){return a?"kelios sekund\u0117s":s?"keli\u0173 sekund\u017ei\u0173":"kelias sekundes"},ss:fa,m:ca,mm:fa,h:ca,hh:fa,d:ca,dd:fa,M:ca,MM:fa,y:ca,yy:fa},dayOfMonthOrdinalParse:/\d{1,2}-oji/,ordinal:function(e){return e+"-oji"},week:{dow:1,doy:4}});var Tn={ss:"sekundes_sekund\u0113m_sekunde_sekundes".split("_"),m:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),mm:"min\u016btes_min\u016bt\u0113m_min\u016bte_min\u016btes".split("_"),h:"stundas_stund\u0101m_stunda_stundas".split("_"),hh:"stundas_stund\u0101m_stunda_stundas".split("_"),d:"dienas_dien\u0101m_diena_dienas".split("_"),dd:"dienas_dien\u0101m_diena_dienas".split("_"),M:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),MM:"m\u0113ne\u0161a_m\u0113ne\u0161iem_m\u0113nesis_m\u0113ne\u0161i".split("_"),y:"gada_gadiem_gads_gadi".split("_"),yy:"gada_gadiem_gads_gadi".split("_")};e.defineLocale("lv",{months:"janv\u0101ris_febru\u0101ris_marts_apr\u012blis_maijs_j\u016bnijs_j\u016blijs_augusts_septembris_oktobris_novembris_decembris".split("_"),monthsShort:"jan_feb_mar_apr_mai_j\u016bn_j\u016bl_aug_sep_okt_nov_dec".split("_"),weekdays:"sv\u0113tdiena_pirmdiena_otrdiena_tre\u0161diena_ceturtdiena_piektdiena_sestdiena".split("_"),weekdaysShort:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysMin:"Sv_P_O_T_C_Pk_S".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY.",LL:"YYYY. [gada] D. MMMM",LLL:"YYYY. [gada] D. MMMM, HH:mm",LLLL:"YYYY. [gada] D. MMMM, dddd, HH:mm"},calendar:{sameDay:"[\u0160odien pulksten] LT",nextDay:"[R\u012bt pulksten] LT",nextWeek:"dddd [pulksten] LT",lastDay:"[Vakar pulksten] LT",lastWeek:"[Pag\u0101ju\u0161\u0101] dddd [pulksten] LT",sameElse:"L"},relativeTime:{future:"p\u0113c %s",past:"pirms %s",s:function(e,a){return a?"da\u017eas sekundes":"da\u017e\u0101m sekund\u0113m"},ss:pa,m:Da,mm:pa,h:Da,hh:pa,d:Da,dd:pa,M:Da,MM:pa,y:Da,yy:pa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var gn={words:{ss:["sekund","sekunda","sekundi"],m:["jedan minut","jednog minuta"],mm:["minut","minuta","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mjesec","mjeseca","mjeseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=gn.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+gn.correctGrammaticalCase(e,s)}};e.defineLocale("me",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedjelja_ponedjeljak_utorak_srijeda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sri._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sjutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedjelju] [u] LT";case 3:return"[u] [srijedu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedjelje] [u] LT","[pro\u0161log] [ponedjeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srijede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"prije %s",s:"nekoliko sekundi",ss:gn.translate,m:gn.translate,mm:gn.translate,h:gn.translate,hh:gn.translate,d:"dan",dd:gn.translate,M:"mjesec",MM:gn.translate,y:"godinu",yy:gn.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("mi",{months:"Kohi-t\u0101te_Hui-tanguru_Pout\u016b-te-rangi_Paenga-wh\u0101wh\u0101_Haratua_Pipiri_H\u014dngoingoi_Here-turi-k\u014dk\u0101_Mahuru_Whiringa-\u0101-nuku_Whiringa-\u0101-rangi_Hakihea".split("_"),monthsShort:"Kohi_Hui_Pou_Pae_Hara_Pipi_H\u014dngoi_Here_Mahu_Whi-nu_Whi-ra_Haki".split("_"),monthsRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,3}/i,monthsShortStrictRegex:/(?:['a-z\u0101\u014D\u016B]+\-?){1,2}/i,weekdays:"R\u0101tapu_Mane_T\u016brei_Wenerei_T\u0101ite_Paraire_H\u0101tarei".split("_"),weekdaysShort:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),weekdaysMin:"Ta_Ma_T\u016b_We_T\u0101i_Pa_H\u0101".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [i] HH:mm",LLLL:"dddd, D MMMM YYYY [i] HH:mm"},calendar:{sameDay:"[i teie mahana, i] LT",nextDay:"[apopo i] LT",nextWeek:"dddd [i] LT",lastDay:"[inanahi i] LT",lastWeek:"dddd [whakamutunga i] LT",sameElse:"L"},relativeTime:{future:"i roto i %s",past:"%s i mua",s:"te h\u0113kona ruarua",ss:"%d h\u0113kona",m:"he meneti",mm:"%d meneti",h:"te haora",hh:"%d haora",d:"he ra",dd:"%d ra",M:"he marama",MM:"%d marama",y:"he tau",yy:"%d tau"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("mk",{months:"\u0458\u0430\u043d\u0443\u0430\u0440\u0438_\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d\u0438_\u0458\u0443\u043b\u0438_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438_\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438_\u043d\u043e\u0435\u043c\u0432\u0440\u0438_\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438".split("_"),monthsShort:"\u0458\u0430\u043d_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433_\u0441\u0435\u043f_\u043e\u043a\u0442_\u043d\u043e\u0435_\u0434\u0435\u043a".split("_"),weekdays:"\u043d\u0435\u0434\u0435\u043b\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u043e\u043a_\u043f\u0435\u0442\u043e\u043a_\u0441\u0430\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434_\u043f\u043e\u043d_\u0432\u0442\u043e_\u0441\u0440\u0435_\u0447\u0435\u0442_\u043f\u0435\u0442_\u0441\u0430\u0431".split("_"),weekdaysMin:"\u043de_\u043fo_\u0432\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441a".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"D.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[\u0414\u0435\u043d\u0435\u0441 \u0432\u043e] LT",nextDay:"[\u0423\u0442\u0440\u0435 \u0432\u043e] LT",nextWeek:"[\u0412\u043e] dddd [\u0432\u043e] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432\u043e] LT",lastWeek:function(){switch(this.day()){case 0:case 3:case 6:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0430\u0442\u0430] dddd [\u0432\u043e] LT";case 1:case 2:case 4:case 5:return"[\u0418\u0437\u043c\u0438\u043d\u0430\u0442\u0438\u043e\u0442] dddd [\u0432\u043e] LT"}},sameElse:"L"},relativeTime:{future:"\u043f\u043e\u0441\u043b\u0435 %s",past:"\u043f\u0440\u0435\u0434 %s",s:"\u043d\u0435\u043a\u043e\u043b\u043a\u0443 \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:"%d \u0441\u0435\u043a\u0443\u043d\u0434\u0438",m:"\u043c\u0438\u043d\u0443\u0442\u0430",mm:"%d \u043c\u0438\u043d\u0443\u0442\u0438",h:"\u0447\u0430\u0441",hh:"%d \u0447\u0430\u0441\u0430",d:"\u0434\u0435\u043d",dd:"%d \u0434\u0435\u043d\u0430",M:"\u043c\u0435\u0441\u0435\u0446",MM:"%d \u043c\u0435\u0441\u0435\u0446\u0438",y:"\u0433\u043e\u0434\u0438\u043d\u0430",yy:"%d \u0433\u043e\u0434\u0438\u043d\u0438"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0435\u0432|\u0435\u043d|\u0442\u0438|\u0432\u0438|\u0440\u0438|\u043c\u0438)/,ordinal:function(e){var a=e%10,t=e%100;return 0===e?e+"-\u0435\u0432":0===t?e+"-\u0435\u043d":t>10&&t<20?e+"-\u0442\u0438":1===a?e+"-\u0432\u0438":2===a?e+"-\u0440\u0438":7===a||8===a?e+"-\u043c\u0438":e+"-\u0442\u0438"},week:{dow:1,doy:7}}),e.defineLocale("ml",{months:"\u0d1c\u0d28\u0d41\u0d35\u0d30\u0d3f_\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41\u0d35\u0d30\u0d3f_\u0d2e\u0d3e\u0d7c\u0d1a\u0d4d\u0d1a\u0d4d_\u0d0f\u0d2a\u0d4d\u0d30\u0d3f\u0d7d_\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48_\u0d13\u0d17\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d_\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02\u0d2c\u0d7c_\u0d12\u0d15\u0d4d\u0d1f\u0d4b\u0d2c\u0d7c_\u0d28\u0d35\u0d02\u0d2c\u0d7c_\u0d21\u0d3f\u0d38\u0d02\u0d2c\u0d7c".split("_"),monthsShort:"\u0d1c\u0d28\u0d41._\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41._\u0d2e\u0d3e\u0d7c._\u0d0f\u0d2a\u0d4d\u0d30\u0d3f._\u0d2e\u0d47\u0d2f\u0d4d_\u0d1c\u0d42\u0d7a_\u0d1c\u0d42\u0d32\u0d48._\u0d13\u0d17._\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31._\u0d12\u0d15\u0d4d\u0d1f\u0d4b._\u0d28\u0d35\u0d02._\u0d21\u0d3f\u0d38\u0d02.".split("_"),monthsParseExact:!0,weekdays:"\u0d1e\u0d3e\u0d2f\u0d31\u0d3e\u0d34\u0d4d\u0d1a_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d3e\u0d34\u0d4d\u0d1a_\u0d1a\u0d4a\u0d35\u0d4d\u0d35\u0d3e\u0d34\u0d4d\u0d1a_\u0d2c\u0d41\u0d27\u0d28\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d3e\u0d34\u0d4d\u0d1a_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a_\u0d36\u0d28\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a".split("_"),weekdaysShort:"\u0d1e\u0d3e\u0d2f\u0d7c_\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d7e_\u0d1a\u0d4a\u0d35\u0d4d\u0d35_\u0d2c\u0d41\u0d27\u0d7b_\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d02_\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f_\u0d36\u0d28\u0d3f".split("_"),weekdaysMin:"\u0d1e\u0d3e_\u0d24\u0d3f_\u0d1a\u0d4a_\u0d2c\u0d41_\u0d35\u0d4d\u0d2f\u0d3e_\u0d35\u0d46_\u0d36".split("_"),longDateFormat:{LT:"A h:mm -\u0d28\u0d41",LTS:"A h:mm:ss -\u0d28\u0d41",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm -\u0d28\u0d41",LLLL:"dddd, D MMMM YYYY, A h:mm -\u0d28\u0d41"},calendar:{sameDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d4d] LT",nextDay:"[\u0d28\u0d3e\u0d33\u0d46] LT",nextWeek:"dddd, LT",lastDay:"[\u0d07\u0d28\u0d4d\u0d28\u0d32\u0d46] LT",lastWeek:"[\u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d",past:"%s \u0d2e\u0d41\u0d7b\u0d2a\u0d4d",s:"\u0d05\u0d7d\u0d2a \u0d28\u0d3f\u0d2e\u0d3f\u0d37\u0d19\u0d4d\u0d19\u0d7e",ss:"%d \u0d38\u0d46\u0d15\u0d4d\u0d15\u0d7b\u0d21\u0d4d",m:"\u0d12\u0d30\u0d41 \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",mm:"%d \u0d2e\u0d3f\u0d28\u0d3f\u0d31\u0d4d\u0d31\u0d4d",h:"\u0d12\u0d30\u0d41 \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",hh:"%d \u0d2e\u0d23\u0d3f\u0d15\u0d4d\u0d15\u0d42\u0d7c",d:"\u0d12\u0d30\u0d41 \u0d26\u0d3f\u0d35\u0d38\u0d02",dd:"%d \u0d26\u0d3f\u0d35\u0d38\u0d02",M:"\u0d12\u0d30\u0d41 \u0d2e\u0d3e\u0d38\u0d02",MM:"%d \u0d2e\u0d3e\u0d38\u0d02",y:"\u0d12\u0d30\u0d41 \u0d35\u0d7c\u0d37\u0d02",yy:"%d \u0d35\u0d7c\u0d37\u0d02"},meridiemParse:/\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f|\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46|\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d|\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02|\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f/i,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"===a&&e>=4||"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d"===a||"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02"===a?e+12:e},meridiem:function(e,a,t){return e<4?"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f":e<12?"\u0d30\u0d3e\u0d35\u0d3f\u0d32\u0d46":e<17?"\u0d09\u0d1a\u0d4d\u0d1a \u0d15\u0d34\u0d3f\u0d1e\u0d4d\u0d1e\u0d4d":e<20?"\u0d35\u0d48\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d47\u0d30\u0d02":"\u0d30\u0d3e\u0d24\u0d4d\u0d30\u0d3f"}});var wn={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},vn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("mr",{months:"\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u090f\u092a\u094d\u0930\u093f\u0932_\u092e\u0947_\u091c\u0942\u0928_\u091c\u0941\u0932\u0948_\u0911\u0917\u0938\u094d\u091f_\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930_\u0911\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930_\u0921\u093f\u0938\u0947\u0902\u092c\u0930".split("_"),monthsShort:"\u091c\u093e\u0928\u0947._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a._\u090f\u092a\u094d\u0930\u093f._\u092e\u0947._\u091c\u0942\u0928._\u091c\u0941\u0932\u0948._\u0911\u0917._\u0938\u092a\u094d\u091f\u0947\u0902._\u0911\u0915\u094d\u091f\u094b._\u0928\u094b\u0935\u094d\u0939\u0947\u0902._\u0921\u093f\u0938\u0947\u0902.".split("_"),monthsParseExact:!0,weekdays:"\u0930\u0935\u093f\u0935\u093e\u0930_\u0938\u094b\u092e\u0935\u093e\u0930_\u092e\u0902\u0917\u0933\u0935\u093e\u0930_\u092c\u0941\u0927\u0935\u093e\u0930_\u0917\u0941\u0930\u0942\u0935\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930_\u0936\u0928\u093f\u0935\u093e\u0930".split("_"),weekdaysShort:"\u0930\u0935\u093f_\u0938\u094b\u092e_\u092e\u0902\u0917\u0933_\u092c\u0941\u0927_\u0917\u0941\u0930\u0942_\u0936\u0941\u0915\u094d\u0930_\u0936\u0928\u093f".split("_"),weekdaysMin:"\u0930_\u0938\u094b_\u092e\u0902_\u092c\u0941_\u0917\u0941_\u0936\u0941_\u0936".split("_"),longDateFormat:{LT:"A h:mm \u0935\u093e\u091c\u0924\u093e",LTS:"A h:mm:ss \u0935\u093e\u091c\u0924\u093e",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e",LLLL:"dddd, D MMMM YYYY, A h:mm \u0935\u093e\u091c\u0924\u093e"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u0909\u0926\u094d\u092f\u093e] LT",nextWeek:"dddd, LT",lastDay:"[\u0915\u093e\u0932] LT",lastWeek:"[\u092e\u093e\u0917\u0940\u0932] dddd, LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u0927\u094d\u092f\u0947",past:"%s\u092a\u0942\u0930\u094d\u0935\u0940",s:Ta,ss:Ta,m:Ta,mm:Ta,h:Ta,hh:Ta,d:Ta,dd:Ta,M:Ta,MM:Ta,y:Ta,yy:Ta},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return vn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return wn[e]})},meridiemParse:/\u0930\u093e\u0924\u094d\u0930\u0940|\u0938\u0915\u093e\u0933\u0940|\u0926\u0941\u092a\u093e\u0930\u0940|\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924\u094d\u0930\u0940"===a?e<4?e:e+12:"\u0938\u0915\u093e\u0933\u0940"===a?e:"\u0926\u0941\u092a\u093e\u0930\u0940"===a?e>=10?e:e+12:"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0930\u093e\u0924\u094d\u0930\u0940":e<10?"\u0938\u0915\u093e\u0933\u0940":e<17?"\u0926\u0941\u092a\u093e\u0930\u0940":e<20?"\u0938\u093e\u092f\u0902\u0915\u093e\u0933\u0940":"\u0930\u093e\u0924\u094d\u0930\u0940"},week:{dow:0,doy:6}}),e.defineLocale("ms-my",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("ms",{months:"Januari_Februari_Mac_April_Mei_Jun_Julai_Ogos_September_Oktober_November_Disember".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ogs_Sep_Okt_Nov_Dis".split("_"),weekdays:"Ahad_Isnin_Selasa_Rabu_Khamis_Jumaat_Sabtu".split("_"),weekdaysShort:"Ahd_Isn_Sel_Rab_Kha_Jum_Sab".split("_"),weekdaysMin:"Ah_Is_Sl_Rb_Km_Jm_Sb".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [pukul] HH.mm",LLLL:"dddd, D MMMM YYYY [pukul] HH.mm"},meridiemParse:/pagi|tengahari|petang|malam/,meridiemHour:function(e,a){return 12===e&&(e=0),"pagi"===a?e:"tengahari"===a?e>=11?e:e+12:"petang"===a||"malam"===a?e+12:void 0},meridiem:function(e,a,t){return e<11?"pagi":e<15?"tengahari":e<19?"petang":"malam"},calendar:{sameDay:"[Hari ini pukul] LT",nextDay:"[Esok pukul] LT",nextWeek:"dddd [pukul] LT",lastDay:"[Kelmarin pukul] LT",lastWeek:"dddd [lepas pukul] LT",sameElse:"L"},relativeTime:{future:"dalam %s",past:"%s yang lepas",s:"beberapa saat",ss:"%d saat",m:"seminit",mm:"%d minit",h:"sejam",hh:"%d jam",d:"sehari",dd:"%d hari",M:"sebulan",MM:"%d bulan",y:"setahun",yy:"%d tahun"},week:{dow:1,doy:7}}),e.defineLocale("mt",{months:"Jannar_Frar_Marzu_April_Mejju_\u0120unju_Lulju_Awwissu_Settembru_Ottubru_Novembru_Di\u010bembru".split("_"),monthsShort:"Jan_Fra_Mar_Apr_Mej_\u0120un_Lul_Aww_Set_Ott_Nov_Di\u010b".split("_"),weekdays:"Il-\u0126add_It-Tnejn_It-Tlieta_L-Erbg\u0127a_Il-\u0126amis_Il-\u0120img\u0127a_Is-Sibt".split("_"),weekdaysShort:"\u0126ad_Tne_Tli_Erb_\u0126am_\u0120im_Sib".split("_"),weekdaysMin:"\u0126a_Tn_Tl_Er_\u0126a_\u0120i_Si".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Illum fil-]LT",nextDay:"[G\u0127ada fil-]LT",nextWeek:"dddd [fil-]LT",lastDay:"[Il-biera\u0127 fil-]LT",lastWeek:"dddd [li g\u0127adda] [fil-]LT",sameElse:"L"},relativeTime:{future:"f\u2019 %s",past:"%s ilu",s:"ftit sekondi",ss:"%d sekondi",m:"minuta",mm:"%d minuti",h:"sieg\u0127a",hh:"%d sieg\u0127at",d:"\u0121urnata",dd:"%d \u0121ranet",M:"xahar",MM:"%d xhur",y:"sena",yy:"%d sni"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}});var Sn={1:"\u1041",2:"\u1042",3:"\u1043",4:"\u1044",5:"\u1045",6:"\u1046",7:"\u1047",8:"\u1048",9:"\u1049",0:"\u1040"},Hn={"\u1041":"1","\u1042":"2","\u1043":"3","\u1044":"4","\u1045":"5","\u1046":"6","\u1047":"7","\u1048":"8","\u1049":"9","\u1040":"0"};e.defineLocale("my",{months:"\u1007\u1014\u103a\u1014\u101d\u102b\u101b\u102e_\u1016\u1031\u1016\u1031\u102c\u103a\u101d\u102b\u101b\u102e_\u1019\u1010\u103a_\u1027\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u1007\u1030\u101c\u102d\u102f\u1004\u103a_\u101e\u103c\u1002\u102f\u1010\u103a_\u1005\u1000\u103a\u1010\u1004\u103a\u1018\u102c_\u1021\u1031\u102c\u1000\u103a\u1010\u102d\u102f\u1018\u102c_\u1014\u102d\u102f\u101d\u1004\u103a\u1018\u102c_\u1012\u102e\u1007\u1004\u103a\u1018\u102c".split("_"),monthsShort:"\u1007\u1014\u103a_\u1016\u1031_\u1019\u1010\u103a_\u1015\u103c\u102e_\u1019\u1031_\u1007\u103d\u1014\u103a_\u101c\u102d\u102f\u1004\u103a_\u101e\u103c_\u1005\u1000\u103a_\u1021\u1031\u102c\u1000\u103a_\u1014\u102d\u102f_\u1012\u102e".split("_"),weekdays:"\u1010\u1014\u1004\u103a\u1039\u1002\u1014\u103d\u1031_\u1010\u1014\u1004\u103a\u1039\u101c\u102c_\u1021\u1004\u103a\u1039\u1002\u102b_\u1017\u102f\u1012\u1039\u1013\u101f\u1030\u1038_\u1000\u103c\u102c\u101e\u1015\u1010\u1031\u1038_\u101e\u1031\u102c\u1000\u103c\u102c_\u1005\u1014\u1031".split("_"),weekdaysShort:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),weekdaysMin:"\u1014\u103d\u1031_\u101c\u102c_\u1002\u102b_\u101f\u1030\u1038_\u1000\u103c\u102c_\u101e\u1031\u102c_\u1014\u1031".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u101a\u1014\u1031.] LT [\u1019\u103e\u102c]",nextDay:"[\u1019\u1014\u1000\u103a\u1016\u103c\u1014\u103a] LT [\u1019\u103e\u102c]",nextWeek:"dddd LT [\u1019\u103e\u102c]",lastDay:"[\u1019\u1014\u1031.\u1000] LT [\u1019\u103e\u102c]",lastWeek:"[\u1015\u103c\u102e\u1038\u1001\u1032\u1037\u101e\u1031\u102c] dddd LT [\u1019\u103e\u102c]",sameElse:"L"},relativeTime:{future:"\u101c\u102c\u1019\u100a\u103a\u1037 %s \u1019\u103e\u102c",past:"\u101c\u103d\u1014\u103a\u1001\u1032\u1037\u101e\u1031\u102c %s \u1000",s:"\u1005\u1000\u1039\u1000\u1014\u103a.\u1021\u1014\u100a\u103a\u1038\u1004\u101a\u103a",ss:"%d \u1005\u1000\u1039\u1000\u1014\u1037\u103a",m:"\u1010\u1005\u103a\u1019\u102d\u1014\u1005\u103a",mm:"%d \u1019\u102d\u1014\u1005\u103a",h:"\u1010\u1005\u103a\u1014\u102c\u101b\u102e",hh:"%d \u1014\u102c\u101b\u102e",d:"\u1010\u1005\u103a\u101b\u1000\u103a",dd:"%d \u101b\u1000\u103a",M:"\u1010\u1005\u103a\u101c",MM:"%d \u101c",y:"\u1010\u1005\u103a\u1014\u103e\u1005\u103a",yy:"%d \u1014\u103e\u1005\u103a"},preparse:function(e){return e.replace(/[\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049\u1040]/g,function(e){return Hn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Sn[e]})},week:{dow:1,doy:4}}),e.defineLocale("nb",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan._feb._mars_april_mai_juni_juli_aug._sep._okt._nov._des.".split("_"),monthsParseExact:!0,weekdays:"s\xf8ndag_mandag_tirsdag_onsdag_torsdag_fredag_l\xf8rdag".split("_"),weekdaysShort:"s\xf8._ma._ti._on._to._fr._l\xf8.".split("_"),weekdaysMin:"s\xf8_ma_ti_on_to_fr_l\xf8".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] HH:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[i dag kl.] LT",nextDay:"[i morgen kl.] LT",nextWeek:"dddd [kl.] LT",lastDay:"[i g\xe5r kl.] LT",lastWeek:"[forrige] dddd [kl.] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s siden",s:"noen sekunder",ss:"%d sekunder",m:"ett minutt",mm:"%d minutter",h:"en time",hh:"%d timer",d:"en dag",dd:"%d dager",M:"en m\xe5ned",MM:"%d m\xe5neder",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var bn={1:"\u0967",2:"\u0968",3:"\u0969",4:"\u096a",5:"\u096b",6:"\u096c",7:"\u096d",8:"\u096e",9:"\u096f",0:"\u0966"},jn={"\u0967":"1","\u0968":"2","\u0969":"3","\u096a":"4","\u096b":"5","\u096c":"6","\u096d":"7","\u096e":"8","\u096f":"9","\u0966":"0"};e.defineLocale("ne",{months:"\u091c\u0928\u0935\u0930\u0940_\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u0930\u0940_\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f\u0932_\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908_\u0905\u0917\u0937\u094d\u091f_\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930_\u0905\u0915\u094d\u091f\u094b\u092c\u0930_\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930_\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930".split("_"),monthsShort:"\u091c\u0928._\u092b\u0947\u092c\u094d\u0930\u0941._\u092e\u093e\u0930\u094d\u091a_\u0905\u092a\u094d\u0930\u093f._\u092e\u0908_\u091c\u0941\u0928_\u091c\u0941\u0932\u093e\u0908._\u0905\u0917._\u0938\u0947\u092a\u094d\u091f._\u0905\u0915\u094d\u091f\u094b._\u0928\u094b\u092d\u0947._\u0921\u093f\u0938\u0947.".split("_"),monthsParseExact:!0,weekdays:"\u0906\u0907\u0924\u092c\u093e\u0930_\u0938\u094b\u092e\u092c\u093e\u0930_\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930_\u092c\u0941\u0927\u092c\u093e\u0930_\u092c\u093f\u0939\u093f\u092c\u093e\u0930_\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930_\u0936\u0928\u093f\u092c\u093e\u0930".split("_"),weekdaysShort:"\u0906\u0907\u0924._\u0938\u094b\u092e._\u092e\u0919\u094d\u0917\u0932._\u092c\u0941\u0927._\u092c\u093f\u0939\u093f._\u0936\u0941\u0915\u094d\u0930._\u0936\u0928\u093f.".split("_"),weekdaysMin:"\u0906._\u0938\u094b._\u092e\u0902._\u092c\u0941._\u092c\u093f._\u0936\u0941._\u0936.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"A\u0915\u094b h:mm \u092c\u091c\u0947",LTS:"A\u0915\u094b h:mm:ss \u092c\u091c\u0947",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947",LLLL:"dddd, D MMMM YYYY, A\u0915\u094b h:mm \u092c\u091c\u0947"},preparse:function(e){return e.replace(/[\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u0966]/g,function(e){return jn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return bn[e]})},meridiemParse:/\u0930\u093e\u0924\u093f|\u092c\u093f\u0939\u093e\u0928|\u0926\u093f\u0909\u0901\u0938\u094b|\u0938\u093e\u0901\u091d/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0930\u093e\u0924\u093f"===a?e<4?e:e+12:"\u092c\u093f\u0939\u093e\u0928"===a?e:"\u0926\u093f\u0909\u0901\u0938\u094b"===a?e>=10?e:e+12:"\u0938\u093e\u0901\u091d"===a?e+12:void 0},meridiem:function(e,a,t){return e<3?"\u0930\u093e\u0924\u093f":e<12?"\u092c\u093f\u0939\u093e\u0928":e<16?"\u0926\u093f\u0909\u0901\u0938\u094b":e<20?"\u0938\u093e\u0901\u091d":"\u0930\u093e\u0924\u093f"},calendar:{sameDay:"[\u0906\u091c] LT",nextDay:"[\u092d\u094b\u0932\u093f] LT",nextWeek:"[\u0906\u0909\u0901\u0926\u094b] dddd[,] LT",lastDay:"[\u0939\u093f\u091c\u094b] LT",lastWeek:"[\u0917\u090f\u0915\u094b] dddd[,] LT",sameElse:"L"},relativeTime:{future:"%s\u092e\u093e",past:"%s \u0905\u0917\u093e\u0921\u093f",s:"\u0915\u0947\u0939\u0940 \u0915\u094d\u0937\u0923",ss:"%d \u0938\u0947\u0915\u0947\u0923\u094d\u0921",m:"\u090f\u0915 \u092e\u093f\u0928\u0947\u091f",mm:"%d \u092e\u093f\u0928\u0947\u091f",h:"\u090f\u0915 \u0918\u0923\u094d\u091f\u093e",hh:"%d \u0918\u0923\u094d\u091f\u093e",d:"\u090f\u0915 \u0926\u093f\u0928",dd:"%d \u0926\u093f\u0928",M:"\u090f\u0915 \u092e\u0939\u093f\u0928\u093e",MM:"%d \u092e\u0939\u093f\u0928\u093e",y:"\u090f\u0915 \u092c\u0930\u094d\u0937",yy:"%d \u092c\u0930\u094d\u0937"},week:{dow:0,doy:6}});var xn="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),Pn="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),On=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],Wn=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;e.defineLocale("nl-be",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?Pn[e.month()]:xn[e.month()]:xn},monthsRegex:Wn,monthsShortRegex:Wn,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:On,longMonthsParse:On,shortMonthsParse:On,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}});var En="jan._feb._mrt._apr._mei_jun._jul._aug._sep._okt._nov._dec.".split("_"),An="jan_feb_mrt_apr_mei_jun_jul_aug_sep_okt_nov_dec".split("_"),Fn=[/^jan/i,/^feb/i,/^maart|mrt.?$/i,/^apr/i,/^mei$/i,/^jun[i.]?$/i,/^jul[i.]?$/i,/^aug/i,/^sep/i,/^okt/i,/^nov/i,/^dec/i],zn=/^(januari|februari|maart|april|mei|april|ju[nl]i|augustus|september|oktober|november|december|jan\.?|feb\.?|mrt\.?|apr\.?|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i;e.defineLocale("nl",{months:"januari_februari_maart_april_mei_juni_juli_augustus_september_oktober_november_december".split("_"),monthsShort:function(e,a){return e?/-MMM-/.test(a)?An[e.month()]:En[e.month()]:En},monthsRegex:zn,monthsShortRegex:zn,monthsStrictRegex:/^(januari|februari|maart|mei|ju[nl]i|april|augustus|september|oktober|november|december)/i,monthsShortStrictRegex:/^(jan\.?|feb\.?|mrt\.?|apr\.?|mei|ju[nl]\.?|aug\.?|sep\.?|okt\.?|nov\.?|dec\.?)/i,monthsParse:Fn,longMonthsParse:Fn,shortMonthsParse:Fn,weekdays:"zondag_maandag_dinsdag_woensdag_donderdag_vrijdag_zaterdag".split("_"),weekdaysShort:"zo._ma._di._wo._do._vr._za.".split("_"),weekdaysMin:"zo_ma_di_wo_do_vr_za".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD-MM-YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[vandaag om] LT",nextDay:"[morgen om] LT",nextWeek:"dddd [om] LT",lastDay:"[gisteren om] LT",lastWeek:"[afgelopen] dddd [om] LT",sameElse:"L"},relativeTime:{future:"over %s",past:"%s geleden",s:"een paar seconden",ss:"%d seconden",m:"\xe9\xe9n minuut",mm:"%d minuten",h:"\xe9\xe9n uur",hh:"%d uur",d:"\xe9\xe9n dag",dd:"%d dagen",M:"\xe9\xe9n maand",MM:"%d maanden",y:"\xe9\xe9n jaar",yy:"%d jaar"},dayOfMonthOrdinalParse:/\d{1,2}(ste|de)/,ordinal:function(e){return e+(1===e||8===e||e>=20?"ste":"de")},week:{dow:1,doy:4}}),e.defineLocale("nn",{months:"januar_februar_mars_april_mai_juni_juli_august_september_oktober_november_desember".split("_"),monthsShort:"jan_feb_mar_apr_mai_jun_jul_aug_sep_okt_nov_des".split("_"),weekdays:"sundag_m\xe5ndag_tysdag_onsdag_torsdag_fredag_laurdag".split("_"),weekdaysShort:"sun_m\xe5n_tys_ons_tor_fre_lau".split("_"),weekdaysMin:"su_m\xe5_ty_on_to_fr_l\xf8".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY [kl.] H:mm",LLLL:"dddd D. MMMM YYYY [kl.] HH:mm"},calendar:{sameDay:"[I dag klokka] LT",nextDay:"[I morgon klokka] LT",nextWeek:"dddd [klokka] LT",lastDay:"[I g\xe5r klokka] LT",lastWeek:"[F\xf8reg\xe5ande] dddd [klokka] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"%s sidan",s:"nokre sekund",ss:"%d sekund",m:"eit minutt",mm:"%d minutt",h:"ein time",hh:"%d timar",d:"ein dag",dd:"%d dagar",M:"ein m\xe5nad",MM:"%d m\xe5nader",y:"eit \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Jn={1:"\u0a67",2:"\u0a68",3:"\u0a69",4:"\u0a6a",5:"\u0a6b",6:"\u0a6c",7:"\u0a6d",8:"\u0a6e",9:"\u0a6f",0:"\u0a66"},Nn={"\u0a67":"1","\u0a68":"2","\u0a69":"3","\u0a6a":"4","\u0a6b":"5","\u0a6c":"6","\u0a6d":"7","\u0a6e":"8","\u0a6f":"9","\u0a66":"0"};e.defineLocale("pa-in",{months:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),monthsShort:"\u0a1c\u0a28\u0a35\u0a30\u0a40_\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40_\u0a2e\u0a3e\u0a30\u0a1a_\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32_\u0a2e\u0a08_\u0a1c\u0a42\u0a28_\u0a1c\u0a41\u0a32\u0a3e\u0a08_\u0a05\u0a17\u0a38\u0a24_\u0a38\u0a24\u0a70\u0a2c\u0a30_\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30_\u0a28\u0a35\u0a70\u0a2c\u0a30_\u0a26\u0a38\u0a70\u0a2c\u0a30".split("_"),weekdays:"\u0a10\u0a24\u0a35\u0a3e\u0a30_\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30_\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30_\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30_\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30_\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30".split("_"),weekdaysShort:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),weekdaysMin:"\u0a10\u0a24_\u0a38\u0a4b\u0a2e_\u0a2e\u0a70\u0a17\u0a32_\u0a2c\u0a41\u0a27_\u0a35\u0a40\u0a30_\u0a38\u0a3c\u0a41\u0a15\u0a30_\u0a38\u0a3c\u0a28\u0a40".split("_"),longDateFormat:{LT:"A h:mm \u0a35\u0a1c\u0a47",LTS:"A h:mm:ss \u0a35\u0a1c\u0a47",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47",LLLL:"dddd, D MMMM YYYY, A h:mm \u0a35\u0a1c\u0a47"},calendar:{sameDay:"[\u0a05\u0a1c] LT",nextDay:"[\u0a15\u0a32] LT",nextWeek:"dddd, LT",lastDay:"[\u0a15\u0a32] LT",lastWeek:"[\u0a2a\u0a3f\u0a1b\u0a32\u0a47] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0a35\u0a3f\u0a71\u0a1a",past:"%s \u0a2a\u0a3f\u0a1b\u0a32\u0a47",s:"\u0a15\u0a41\u0a1d \u0a38\u0a15\u0a3f\u0a70\u0a1f",ss:"%d \u0a38\u0a15\u0a3f\u0a70\u0a1f",m:"\u0a07\u0a15 \u0a2e\u0a3f\u0a70\u0a1f",mm:"%d \u0a2e\u0a3f\u0a70\u0a1f",h:"\u0a07\u0a71\u0a15 \u0a18\u0a70\u0a1f\u0a3e",hh:"%d \u0a18\u0a70\u0a1f\u0a47",d:"\u0a07\u0a71\u0a15 \u0a26\u0a3f\u0a28",dd:"%d \u0a26\u0a3f\u0a28",M:"\u0a07\u0a71\u0a15 \u0a2e\u0a39\u0a40\u0a28\u0a3e",MM:"%d \u0a2e\u0a39\u0a40\u0a28\u0a47",y:"\u0a07\u0a71\u0a15 \u0a38\u0a3e\u0a32",yy:"%d \u0a38\u0a3e\u0a32"},preparse:function(e){return e.replace(/[\u0a67\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0a66]/g,function(e){return Nn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Jn[e]})},meridiemParse:/\u0a30\u0a3e\u0a24|\u0a38\u0a35\u0a47\u0a30|\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30|\u0a38\u0a3c\u0a3e\u0a2e/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0a30\u0a3e\u0a24"===a?e<4?e:e+12:"\u0a38\u0a35\u0a47\u0a30"===a?e:"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30"===a?e>=10?e:e+12:"\u0a38\u0a3c\u0a3e\u0a2e"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0a30\u0a3e\u0a24":e<10?"\u0a38\u0a35\u0a47\u0a30":e<17?"\u0a26\u0a41\u0a2a\u0a39\u0a3f\u0a30":e<20?"\u0a38\u0a3c\u0a3e\u0a2e":"\u0a30\u0a3e\u0a24"},week:{dow:0,doy:6}});var Rn="stycze\u0144_luty_marzec_kwiecie\u0144_maj_czerwiec_lipiec_sierpie\u0144_wrzesie\u0144_pa\u017adziernik_listopad_grudzie\u0144".split("_"),In="stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_wrze\u015bnia_pa\u017adziernika_listopada_grudnia".split("_");e.defineLocale("pl",{months:function(e,a){return e?""===a?"("+In[e.month()]+"|"+Rn[e.month()]+")":/D MMMM/.test(a)?In[e.month()]:Rn[e.month()]:Rn},monthsShort:"sty_lut_mar_kwi_maj_cze_lip_sie_wrz_pa\u017a_lis_gru".split("_"),weekdays:"niedziela_poniedzia\u0142ek_wtorek_\u015broda_czwartek_pi\u0105tek_sobota".split("_"),weekdaysShort:"ndz_pon_wt_\u015br_czw_pt_sob".split("_"),weekdaysMin:"Nd_Pn_Wt_\u015ar_Cz_Pt_So".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Dzi\u015b o] LT",nextDay:"[Jutro o] LT",nextWeek:function(){switch(this.day()){case 0:return"[W niedziel\u0119 o] LT";case 2:return"[We wtorek o] LT";case 3:return"[W \u015brod\u0119 o] LT";case 6:return"[W sobot\u0119 o] LT";default:return"[W] dddd [o] LT"}},lastDay:"[Wczoraj o] LT",lastWeek:function(){switch(this.day()){case 0:return"[W zesz\u0142\u0105 niedziel\u0119 o] LT";case 3:return"[W zesz\u0142\u0105 \u015brod\u0119 o] LT";case 6:return"[W zesz\u0142\u0105 sobot\u0119 o] LT";default:return"[W zesz\u0142y] dddd [o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"%s temu",s:"kilka sekund",ss:wa,m:wa,mm:wa,h:wa,hh:wa,d:"1 dzie\u0144",dd:"%d dni",M:"miesi\u0105c",MM:wa,y:"rok",yy:wa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("pt-br",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"Domingo_Segunda-feira_Ter\xe7a-feira_Quarta-feira_Quinta-feira_Sexta-feira_S\xe1bado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_S\xe1b".split("_"),weekdaysMin:"Do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [\xe0s] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [\xe0s] HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atr\xe1s",s:"poucos segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba"}),e.defineLocale("pt",{months:"janeiro_fevereiro_mar\xe7o_abril_maio_junho_julho_agosto_setembro_outubro_novembro_dezembro".split("_"),monthsShort:"jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez".split("_"),weekdays:"Domingo_Segunda-feira_Ter\xe7a-feira_Quarta-feira_Quinta-feira_Sexta-feira_S\xe1bado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_S\xe1b".split("_"),weekdaysMin:"Do_2\xaa_3\xaa_4\xaa_5\xaa_6\xaa_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY HH:mm"},calendar:{sameDay:"[Hoje \xe0s] LT",nextDay:"[Amanh\xe3 \xe0s] LT",nextWeek:"dddd [\xe0s] LT",lastDay:"[Ontem \xe0s] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[\xdaltimo] dddd [\xe0s] LT":"[\xdaltima] dddd [\xe0s] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"h\xe1 %s",s:"segundos",ss:"%d segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um m\xeas",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}\xba/,ordinal:"%d\xba",week:{dow:1,doy:4}}),e.defineLocale("ro",{months:"ianuarie_februarie_martie_aprilie_mai_iunie_iulie_august_septembrie_octombrie_noiembrie_decembrie".split("_"),monthsShort:"ian._febr._mart._apr._mai_iun._iul._aug._sept._oct._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"duminic\u0103_luni_mar\u021bi_miercuri_joi_vineri_s\xe2mb\u0103t\u0103".split("_"),weekdaysShort:"Dum_Lun_Mar_Mie_Joi_Vin_S\xe2m".split("_"),weekdaysMin:"Du_Lu_Ma_Mi_Jo_Vi_S\xe2".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY H:mm",LLLL:"dddd, D MMMM YYYY H:mm"},calendar:{sameDay:"[azi la] LT",nextDay:"[m\xe2ine la] LT",nextWeek:"dddd [la] LT",lastDay:"[ieri la] LT",lastWeek:"[fosta] dddd [la] LT",sameElse:"L"},relativeTime:{future:"peste %s",past:"%s \xeen urm\u0103",s:"c\xe2teva secunde",ss:va,m:"un minut",mm:va,h:"o or\u0103",hh:va,d:"o zi",dd:va,M:"o lun\u0103",MM:va,y:"un an",yy:va},week:{dow:1,doy:7}});var Cn=[/^\u044f\u043d\u0432/i,/^\u0444\u0435\u0432/i,/^\u043c\u0430\u0440/i,/^\u0430\u043f\u0440/i,/^\u043c\u0430[\u0439\u044f]/i,/^\u0438\u044e\u043d/i,/^\u0438\u044e\u043b/i,/^\u0430\u0432\u0433/i,/^\u0441\u0435\u043d/i,/^\u043e\u043a\u0442/i,/^\u043d\u043e\u044f/i,/^\u0434\u0435\u043a/i];e.defineLocale("ru",{months:{format:"\u044f\u043d\u0432\u0430\u0440\u044f_\u0444\u0435\u0432\u0440\u0430\u043b\u044f_\u043c\u0430\u0440\u0442\u0430_\u0430\u043f\u0440\u0435\u043b\u044f_\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433\u0443\u0441\u0442\u0430_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f_\u043e\u043a\u0442\u044f\u0431\u0440\u044f_\u043d\u043e\u044f\u0431\u0440\u044f_\u0434\u0435\u043a\u0430\u0431\u0440\u044f".split("_"),standalone:"\u044f\u043d\u0432\u0430\u0440\u044c_\u0444\u0435\u0432\u0440\u0430\u043b\u044c_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b\u044c_\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c_\u043e\u043a\u0442\u044f\u0431\u0440\u044c_\u043d\u043e\u044f\u0431\u0440\u044c_\u0434\u0435\u043a\u0430\u0431\u0440\u044c".split("_")},monthsShort:{format:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u044f_\u0438\u044e\u043d\u044f_\u0438\u044e\u043b\u044f_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_"),standalone:"\u044f\u043d\u0432._\u0444\u0435\u0432\u0440._\u043c\u0430\u0440\u0442_\u0430\u043f\u0440._\u043c\u0430\u0439_\u0438\u044e\u043d\u044c_\u0438\u044e\u043b\u044c_\u0430\u0432\u0433._\u0441\u0435\u043d\u0442._\u043e\u043a\u0442._\u043d\u043e\u044f\u0431._\u0434\u0435\u043a.".split("_")},weekdays:{standalone:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0430_\u0441\u0443\u0431\u0431\u043e\u0442\u0430".split("_"),format:"\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435_\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a_\u0432\u0442\u043e\u0440\u043d\u0438\u043a_\u0441\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440\u0433_\u043f\u044f\u0442\u043d\u0438\u0446\u0443_\u0441\u0443\u0431\u0431\u043e\u0442\u0443".split("_"),isFormat:/\[ ?[\u0412\u0432] ?(?:\u043f\u0440\u043e\u0448\u043b\u0443\u044e|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e|\u044d\u0442\u0443)? ?\] ?dddd/},weekdaysShort:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u0432\u0441_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),monthsParse:Cn,longMonthsParse:Cn,shortMonthsParse:Cn,monthsRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsShortRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044c\u044f]|\u044f\u043d\u0432\.?|\u0444\u0435\u0432\u0440\u0430\u043b[\u044c\u044f]|\u0444\u0435\u0432\u0440?\.?|\u043c\u0430\u0440\u0442\u0430?|\u043c\u0430\u0440\.?|\u0430\u043f\u0440\u0435\u043b[\u044c\u044f]|\u0430\u043f\u0440\.?|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d[\u044c\u044f]|\u0438\u044e\u043d\.?|\u0438\u044e\u043b[\u044c\u044f]|\u0438\u044e\u043b\.?|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0430\u0432\u0433\.?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044c\u044f]|\u0441\u0435\u043d\u0442?\.?|\u043e\u043a\u0442\u044f\u0431\u0440[\u044c\u044f]|\u043e\u043a\u0442\.?|\u043d\u043e\u044f\u0431\u0440[\u044c\u044f]|\u043d\u043e\u044f\u0431?\.?|\u0434\u0435\u043a\u0430\u0431\u0440[\u044c\u044f]|\u0434\u0435\u043a\.?)/i,monthsStrictRegex:/^(\u044f\u043d\u0432\u0430\u0440[\u044f\u044c]|\u0444\u0435\u0432\u0440\u0430\u043b[\u044f\u044c]|\u043c\u0430\u0440\u0442\u0430?|\u0430\u043f\u0440\u0435\u043b[\u044f\u044c]|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044f\u044c]|\u0438\u044e\u043b[\u044f\u044c]|\u0430\u0432\u0433\u0443\u0441\u0442\u0430?|\u0441\u0435\u043d\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043e\u043a\u0442\u044f\u0431\u0440[\u044f\u044c]|\u043d\u043e\u044f\u0431\u0440[\u044f\u044c]|\u0434\u0435\u043a\u0430\u0431\u0440[\u044f\u044c])/i,monthsShortStrictRegex:/^(\u044f\u043d\u0432\.|\u0444\u0435\u0432\u0440?\.|\u043c\u0430\u0440[\u0442.]|\u0430\u043f\u0440\.|\u043c\u0430[\u044f\u0439]|\u0438\u044e\u043d[\u044c\u044f.]|\u0438\u044e\u043b[\u044c\u044f.]|\u0430\u0432\u0433\.|\u0441\u0435\u043d\u0442?\.|\u043e\u043a\u0442\.|\u043d\u043e\u044f\u0431?\.|\u0434\u0435\u043a\.)/i,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0433.",LLL:"D MMMM YYYY \u0433., H:mm",LLLL:"dddd, D MMMM YYYY \u0433., H:mm"},calendar:{sameDay:"[\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0432] LT",nextDay:"[\u0417\u0430\u0432\u0442\u0440\u0430 \u0432] LT",lastDay:"[\u0412\u0447\u0435\u0440\u0430 \u0432] LT",nextWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd [\u0432] LT":"[\u0412] dddd [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0435] dddd [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439] dddd [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e] dddd [\u0432] LT"}},lastWeek:function(e){if(e.week()===this.week())return 2===this.day()?"[\u0412\u043e] dddd [\u0432] LT":"[\u0412] dddd [\u0432] LT";switch(this.day()){case 0:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u043e\u0435] dddd [\u0432] LT";case 1:case 2:case 4:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u044b\u0439] dddd [\u0432] LT";case 3:case 5:case 6:return"[\u0412 \u043f\u0440\u043e\u0448\u043b\u0443\u044e] dddd [\u0432] LT"}},sameElse:"L"},relativeTime:{future:"\u0447\u0435\u0440\u0435\u0437 %s",past:"%s \u043d\u0430\u0437\u0430\u0434",s:"\u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434",ss:Sa,m:Sa,mm:Sa,h:"\u0447\u0430\u0441",hh:Sa,d:"\u0434\u0435\u043d\u044c",dd:Sa,M:"\u043c\u0435\u0441\u044f\u0446",MM:Sa,y:"\u0433\u043e\u0434",yy:Sa},meridiemParse:/\u043d\u043e\u0447\u0438|\u0443\u0442\u0440\u0430|\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430/i,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u0435\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u0438":e<12?"\u0443\u0442\u0440\u0430":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u0435\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e|\u044f)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":return e+"-\u0439";case"D":return e+"-\u0433\u043e";case"w":case"W":return e+"-\u044f";default:return e}},week:{dow:1,doy:4}});var Gn=["\u062c\u0646\u0648\u0631\u064a","\u0641\u064a\u0628\u0631\u0648\u0631\u064a","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u064a\u0644","\u0645\u0626\u064a","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0621\u0650","\u0622\u06af\u0633\u067d","\u0633\u064a\u067e\u067d\u0645\u0628\u0631","\u0622\u06aa\u067d\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u068a\u0633\u0645\u0628\u0631"],Un=["\u0622\u0686\u0631","\u0633\u0648\u0645\u0631","\u0627\u06b1\u0627\u0631\u0648","\u0627\u0631\u0628\u0639","\u062e\u0645\u064a\u0633","\u062c\u0645\u0639","\u0687\u0646\u0687\u0631"];e.defineLocale("sd",{months:Gn,monthsShort:Gn,weekdays:Un,weekdaysShort:Un,weekdaysMin:Un,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0627\u0684] LT",nextDay:"[\u0633\u0680\u0627\u06bb\u064a] LT",nextWeek:"dddd [\u0627\u06b3\u064a\u0646 \u0647\u0641\u062a\u064a \u062a\u064a] LT",lastDay:"[\u06aa\u0627\u0644\u0647\u0647] LT",lastWeek:"[\u06af\u0632\u0631\u064a\u0644 \u0647\u0641\u062a\u064a] dddd [\u062a\u064a] LT",sameElse:"L"},relativeTime:{future:"%s \u067e\u0648\u0621",past:"%s \u0627\u06b3",s:"\u0686\u0646\u062f \u0633\u064a\u06aa\u0646\u068a",ss:"%d \u0633\u064a\u06aa\u0646\u068a",m:"\u0647\u06aa \u0645\u0646\u067d",mm:"%d \u0645\u0646\u067d",h:"\u0647\u06aa \u06aa\u0644\u0627\u06aa",hh:"%d \u06aa\u0644\u0627\u06aa",d:"\u0647\u06aa \u068f\u064a\u0646\u0647\u0646",dd:"%d \u068f\u064a\u0646\u0647\u0646",M:"\u0647\u06aa \u0645\u0647\u064a\u0646\u0648",MM:"%d \u0645\u0647\u064a\u0646\u0627",y:"\u0647\u06aa \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}}),e.defineLocale("se",{months:"o\u0111\u0111ajagem\xe1nnu_guovvam\xe1nnu_njuk\u010dam\xe1nnu_cuo\u014bom\xe1nnu_miessem\xe1nnu_geassem\xe1nnu_suoidnem\xe1nnu_borgem\xe1nnu_\u010dak\u010dam\xe1nnu_golggotm\xe1nnu_sk\xe1bmam\xe1nnu_juovlam\xe1nnu".split("_"),monthsShort:"o\u0111\u0111j_guov_njuk_cuo_mies_geas_suoi_borg_\u010dak\u010d_golg_sk\xe1b_juov".split("_"),weekdays:"sotnabeaivi_vuoss\xe1rga_ma\u014b\u014beb\xe1rga_gaskavahkku_duorastat_bearjadat_l\xe1vvardat".split("_"),weekdaysShort:"sotn_vuos_ma\u014b_gask_duor_bear_l\xe1v".split("_"),weekdaysMin:"s_v_m_g_d_b_L".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"MMMM D. [b.] YYYY",LLL:"MMMM D. [b.] YYYY [ti.] HH:mm",LLLL:"dddd, MMMM D. [b.] YYYY [ti.] HH:mm"},calendar:{sameDay:"[otne ti] LT",nextDay:"[ihttin ti] LT",nextWeek:"dddd [ti] LT",lastDay:"[ikte ti] LT",lastWeek:"[ovddit] dddd [ti] LT",sameElse:"L"},relativeTime:{future:"%s gea\u017ees",past:"ma\u014bit %s",s:"moadde sekunddat",ss:"%d sekunddat",m:"okta minuhta",mm:"%d minuhtat",h:"okta diimmu",hh:"%d diimmut",d:"okta beaivi",dd:"%d beaivvit",M:"okta m\xe1nnu",MM:"%d m\xe1nut",y:"okta jahki",yy:"%d jagit"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("si",{months:"\u0da2\u0db1\u0dc0\u0dcf\u0dbb\u0dd2_\u0db4\u0dd9\u0db6\u0dbb\u0dc0\u0dcf\u0dbb\u0dd2_\u0db8\u0dcf\u0dbb\u0dca\u0dad\u0dd4_\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd\u0dc3\u0dca\u0dad\u0dd4_\u0dc3\u0dd0\u0db4\u0dca\u0dad\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0d94\u0d9a\u0dca\u0dad\u0ddd\u0db6\u0dbb\u0dca_\u0db1\u0ddc\u0dc0\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca_\u0daf\u0dd9\u0dc3\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca".split("_"),monthsShort:"\u0da2\u0db1_\u0db4\u0dd9\u0db6_\u0db8\u0dcf\u0dbb\u0dca_\u0d85\u0db4\u0dca_\u0db8\u0dd0\u0dba\u0dd2_\u0da2\u0dd6\u0db1\u0dd2_\u0da2\u0dd6\u0dbd\u0dd2_\u0d85\u0d9c\u0ddd_\u0dc3\u0dd0\u0db4\u0dca_\u0d94\u0d9a\u0dca_\u0db1\u0ddc\u0dc0\u0dd0_\u0daf\u0dd9\u0dc3\u0dd0".split("_"),weekdays:"\u0d89\u0dbb\u0dd2\u0daf\u0dcf_\u0dc3\u0db3\u0dd4\u0daf\u0dcf_\u0d85\u0d9f\u0dc4\u0dbb\u0dd4\u0dc0\u0dcf\u0daf\u0dcf_\u0db6\u0daf\u0dcf\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4\u0dc3\u0dca\u0db4\u0dad\u0dd2\u0db1\u0dca\u0daf\u0dcf_\u0dc3\u0dd2\u0d9a\u0dd4\u0dbb\u0dcf\u0daf\u0dcf_\u0dc3\u0dd9\u0db1\u0dc3\u0dd4\u0dbb\u0dcf\u0daf\u0dcf".split("_"),weekdaysShort:"\u0d89\u0dbb\u0dd2_\u0dc3\u0db3\u0dd4_\u0d85\u0d9f_\u0db6\u0daf\u0dcf_\u0db6\u0dca\u200d\u0dbb\u0dc4_\u0dc3\u0dd2\u0d9a\u0dd4_\u0dc3\u0dd9\u0db1".split("_"),weekdaysMin:"\u0d89_\u0dc3_\u0d85_\u0db6_\u0db6\u0dca\u200d\u0dbb_\u0dc3\u0dd2_\u0dc3\u0dd9".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"a h:mm",LTS:"a h:mm:ss",L:"YYYY/MM/DD",LL:"YYYY MMMM D",LLL:"YYYY MMMM D, a h:mm",LLLL:"YYYY MMMM D [\u0dc0\u0dd0\u0db1\u0dd2] dddd, a h:mm:ss"},calendar:{sameDay:"[\u0d85\u0daf] LT[\u0da7]",nextDay:"[\u0dc4\u0dd9\u0da7] LT[\u0da7]",nextWeek:"dddd LT[\u0da7]",lastDay:"[\u0d8a\u0dba\u0dda] LT[\u0da7]",lastWeek:"[\u0db4\u0dc3\u0dd4\u0d9c\u0dd2\u0dba] dddd LT[\u0da7]",sameElse:"L"},relativeTime:{future:"%s\u0d9a\u0dd2\u0db1\u0dca",past:"%s\u0d9a\u0da7 \u0db4\u0dd9\u0dbb",s:"\u0dad\u0dad\u0dca\u0db4\u0dbb \u0d9a\u0dd2\u0dc4\u0dd2\u0db4\u0dba",ss:"\u0dad\u0dad\u0dca\u0db4\u0dbb %d",m:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4\u0dc0",mm:"\u0db8\u0dd2\u0db1\u0dd2\u0dad\u0dca\u0dad\u0dd4 %d",h:"\u0db4\u0dd0\u0dba",hh:"\u0db4\u0dd0\u0dba %d",d:"\u0daf\u0dd2\u0db1\u0dba",dd:"\u0daf\u0dd2\u0db1 %d",M:"\u0db8\u0dcf\u0dc3\u0dba",MM:"\u0db8\u0dcf\u0dc3 %d",y:"\u0dc0\u0dc3\u0dbb",yy:"\u0dc0\u0dc3\u0dbb %d"},dayOfMonthOrdinalParse:/\d{1,2} \u0dc0\u0dd0\u0db1\u0dd2/,ordinal:function(e){return e+" \u0dc0\u0dd0\u0db1\u0dd2"},meridiemParse:/\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4|\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4|\u0db4\u0dd9.\u0dc0|\u0db4.\u0dc0./,isPM:function(e){return"\u0db4.\u0dc0."===e||"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4"===e},meridiem:function(e,a,t){return e>11?t?"\u0db4.\u0dc0.":"\u0db4\u0dc3\u0dca \u0dc0\u0dbb\u0dd4":t?"\u0db4\u0dd9.\u0dc0.":"\u0db4\u0dd9\u0dbb \u0dc0\u0dbb\u0dd4"}});var Vn="janu\xe1r_febru\xe1r_marec_apr\xedl_m\xe1j_j\xfan_j\xfal_august_september_okt\xf3ber_november_december".split("_"),Kn="jan_feb_mar_apr_m\xe1j_j\xfan_j\xfal_aug_sep_okt_nov_dec".split("_");e.defineLocale("sk",{months:Vn,monthsShort:Kn,weekdays:"nede\u013ea_pondelok_utorok_streda_\u0161tvrtok_piatok_sobota".split("_"),weekdaysShort:"ne_po_ut_st_\u0161t_pi_so".split("_"),weekdaysMin:"ne_po_ut_st_\u0161t_pi_so".split("_"),longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd D. MMMM YYYY H:mm"},calendar:{sameDay:"[dnes o] LT",nextDay:"[zajtra o] LT",nextWeek:function(){switch(this.day()){case 0:return"[v nede\u013eu o] LT";case 1:case 2:return"[v] dddd [o] LT";case 3:return"[v stredu o] LT";case 4:return"[vo \u0161tvrtok o] LT";case 5:return"[v piatok o] LT";case 6:return"[v sobotu o] LT"}},lastDay:"[v\u010dera o] LT",lastWeek:function(){switch(this.day()){case 0:return"[minul\xfa nede\u013eu o] LT";case 1:case 2:return"[minul\xfd] dddd [o] LT";case 3:return"[minul\xfa stredu o] LT";case 4:case 5:return"[minul\xfd] dddd [o] LT";case 6:return"[minul\xfa sobotu o] LT"}},sameElse:"L"},relativeTime:{future:"za %s",past:"pred %s",s:ba,ss:ba,m:ba,mm:ba,h:ba,hh:ba,d:ba,dd:ba,M:ba,MM:ba,y:ba,yy:ba},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("sl",{months:"januar_februar_marec_april_maj_junij_julij_avgust_september_oktober_november_december".split("_"),monthsShort:"jan._feb._mar._apr._maj._jun._jul._avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljek_torek_sreda_\u010detrtek_petek_sobota".split("_"),weekdaysShort:"ned._pon._tor._sre._\u010det._pet._sob.".split("_"),weekdaysMin:"ne_po_to_sr_\u010de_pe_so".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danes ob] LT",nextDay:"[jutri ob] LT",nextWeek:function(){switch(this.day()){case 0:return"[v] [nedeljo] [ob] LT";case 3:return"[v] [sredo] [ob] LT";case 6:return"[v] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[v] dddd [ob] LT"}},lastDay:"[v\u010deraj ob] LT",lastWeek:function(){switch(this.day()){case 0:return"[prej\u0161njo] [nedeljo] [ob] LT";case 3:return"[prej\u0161njo] [sredo] [ob] LT";case 6:return"[prej\u0161njo] [soboto] [ob] LT";case 1:case 2:case 4:case 5:return"[prej\u0161nji] dddd [ob] LT"}},sameElse:"L"},relativeTime:{future:"\u010dez %s",past:"pred %s",s:ja,ss:ja,m:ja,mm:ja,h:ja,hh:ja,d:ja,dd:ja,M:ja,MM:ja,y:ja,yy:ja},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("sq",{months:"Janar_Shkurt_Mars_Prill_Maj_Qershor_Korrik_Gusht_Shtator_Tetor_N\xebntor_Dhjetor".split("_"),monthsShort:"Jan_Shk_Mar_Pri_Maj_Qer_Kor_Gus_Sht_Tet_N\xebn_Dhj".split("_"),weekdays:"E Diel_E H\xebn\xeb_E Mart\xeb_E M\xebrkur\xeb_E Enjte_E Premte_E Shtun\xeb".split("_"),weekdaysShort:"Die_H\xebn_Mar_M\xebr_Enj_Pre_Sht".split("_"),weekdaysMin:"D_H_Ma_M\xeb_E_P_Sh".split("_"),weekdaysParseExact:!0,meridiemParse:/PD|MD/,isPM:function(e){return"M"===e.charAt(0)},meridiem:function(e,a,t){return e<12?"PD":"MD"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Sot n\xeb] LT",nextDay:"[Nes\xebr n\xeb] LT",nextWeek:"dddd [n\xeb] LT",lastDay:"[Dje n\xeb] LT",lastWeek:"dddd [e kaluar n\xeb] LT",sameElse:"L"},relativeTime:{future:"n\xeb %s",past:"%s m\xeb par\xeb",s:"disa sekonda",ss:"%d sekonda",m:"nj\xeb minut\xeb",mm:"%d minuta",h:"nj\xeb or\xeb",hh:"%d or\xeb",d:"nj\xeb dit\xeb",dd:"%d dit\xeb",M:"nj\xeb muaj",MM:"%d muaj",y:"nj\xeb vit",yy:"%d vite"},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Zn={words:{ss:["\u0441\u0435\u043a\u0443\u043d\u0434\u0430","\u0441\u0435\u043a\u0443\u043d\u0434\u0435","\u0441\u0435\u043a\u0443\u043d\u0434\u0438"],m:["\u0458\u0435\u0434\u0430\u043d \u043c\u0438\u043d\u0443\u0442","\u0458\u0435\u0434\u043d\u0435 \u043c\u0438\u043d\u0443\u0442\u0435"],mm:["\u043c\u0438\u043d\u0443\u0442","\u043c\u0438\u043d\u0443\u0442\u0435","\u043c\u0438\u043d\u0443\u0442\u0430"],h:["\u0458\u0435\u0434\u0430\u043d \u0441\u0430\u0442","\u0458\u0435\u0434\u043d\u043e\u0433 \u0441\u0430\u0442\u0430"],hh:["\u0441\u0430\u0442","\u0441\u0430\u0442\u0430","\u0441\u0430\u0442\u0438"],dd:["\u0434\u0430\u043d","\u0434\u0430\u043d\u0430","\u0434\u0430\u043d\u0430"],MM:["\u043c\u0435\u0441\u0435\u0446","\u043c\u0435\u0441\u0435\u0446\u0430","\u043c\u0435\u0441\u0435\u0446\u0438"],yy:["\u0433\u043e\u0434\u0438\u043d\u0430","\u0433\u043e\u0434\u0438\u043d\u0435","\u0433\u043e\u0434\u0438\u043d\u0430"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=Zn.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+Zn.correctGrammaticalCase(e,s)}};e.defineLocale("sr-cyrl",{months:"\u0458\u0430\u043d\u0443\u0430\u0440_\u0444\u0435\u0431\u0440\u0443\u0430\u0440_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0438\u043b_\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440_\u043e\u043a\u0442\u043e\u0431\u0430\u0440_\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440_\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440".split("_"),monthsShort:"\u0458\u0430\u043d._\u0444\u0435\u0431._\u043c\u0430\u0440._\u0430\u043f\u0440._\u043c\u0430\u0458_\u0458\u0443\u043d_\u0458\u0443\u043b_\u0430\u0432\u0433._\u0441\u0435\u043f._\u043e\u043a\u0442._\u043d\u043e\u0432._\u0434\u0435\u0446.".split("_"),monthsParseExact:!0,weekdays:"\u043d\u0435\u0434\u0435\u0459\u0430_\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a_\u0443\u0442\u043e\u0440\u0430\u043a_\u0441\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a_\u043f\u0435\u0442\u0430\u043a_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),weekdaysShort:"\u043d\u0435\u0434._\u043f\u043e\u043d._\u0443\u0442\u043e._\u0441\u0440\u0435._\u0447\u0435\u0442._\u043f\u0435\u0442._\u0441\u0443\u0431.".split("_"),weekdaysMin:"\u043d\u0435_\u043f\u043e_\u0443\u0442_\u0441\u0440_\u0447\u0435_\u043f\u0435_\u0441\u0443".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[\u0434\u0430\u043d\u0430\u0441 \u0443] LT",nextDay:"[\u0441\u0443\u0442\u0440\u0430 \u0443] LT",nextWeek:function(){switch(this.day()){case 0:return"[\u0443] [\u043d\u0435\u0434\u0435\u0459\u0443] [\u0443] LT";case 3:return"[\u0443] [\u0441\u0440\u0435\u0434\u0443] [\u0443] LT";case 6:return"[\u0443] [\u0441\u0443\u0431\u043e\u0442\u0443] [\u0443] LT";case 1:case 2:case 4:case 5:return"[\u0443] dddd [\u0443] LT"}},lastDay:"[\u0458\u0443\u0447\u0435 \u0443] LT",lastWeek:function(){return["[\u043f\u0440\u043e\u0448\u043b\u0435] [\u043d\u0435\u0434\u0435\u0459\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0443\u0442\u043e\u0440\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0440\u0435\u0434\u0435] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u0447\u0435\u0442\u0432\u0440\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u043e\u0433] [\u043f\u0435\u0442\u043a\u0430] [\u0443] LT","[\u043f\u0440\u043e\u0448\u043b\u0435] [\u0441\u0443\u0431\u043e\u0442\u0435] [\u0443] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"\u043f\u0440\u0435 %s",s:"\u043d\u0435\u043a\u043e\u043b\u0438\u043a\u043e \u0441\u0435\u043a\u0443\u043d\u0434\u0438",ss:Zn.translate,m:Zn.translate,mm:Zn.translate,h:Zn.translate,hh:Zn.translate,d:"\u0434\u0430\u043d",dd:Zn.translate,M:"\u043c\u0435\u0441\u0435\u0446",MM:Zn.translate,y:"\u0433\u043e\u0434\u0438\u043d\u0443",yy:Zn.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}});var $n={words:{ss:["sekunda","sekunde","sekundi"],m:["jedan minut","jedne minute"],mm:["minut","minute","minuta"],h:["jedan sat","jednog sata"],hh:["sat","sata","sati"],dd:["dan","dana","dana"],MM:["mesec","meseca","meseci"],yy:["godina","godine","godina"]},correctGrammaticalCase:function(e,a){return 1===e?a[0]:e>=2&&e<=4?a[1]:a[2]},translate:function(e,a,t){var s=$n.words[t];return 1===t.length?a?s[0]:s[1]:e+" "+$n.correctGrammaticalCase(e,s)}};e.defineLocale("sr",{months:"januar_februar_mart_april_maj_jun_jul_avgust_septembar_oktobar_novembar_decembar".split("_"),monthsShort:"jan._feb._mar._apr._maj_jun_jul_avg._sep._okt._nov._dec.".split("_"),monthsParseExact:!0,weekdays:"nedelja_ponedeljak_utorak_sreda_\u010detvrtak_petak_subota".split("_"),weekdaysShort:"ned._pon._uto._sre._\u010det._pet._sub.".split("_"),weekdaysMin:"ne_po_ut_sr_\u010de_pe_su".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD.MM.YYYY",LL:"D. MMMM YYYY",LLL:"D. MMMM YYYY H:mm",LLLL:"dddd, D. MMMM YYYY H:mm"},calendar:{sameDay:"[danas u] LT",nextDay:"[sutra u] LT",nextWeek:function(){switch(this.day()){case 0:return"[u] [nedelju] [u] LT";case 3:return"[u] [sredu] [u] LT";case 6:return"[u] [subotu] [u] LT";case 1:case 2:case 4:case 5:return"[u] dddd [u] LT"}},lastDay:"[ju\u010de u] LT",lastWeek:function(){return["[pro\u0161le] [nedelje] [u] LT","[pro\u0161log] [ponedeljka] [u] LT","[pro\u0161log] [utorka] [u] LT","[pro\u0161le] [srede] [u] LT","[pro\u0161log] [\u010detvrtka] [u] LT","[pro\u0161log] [petka] [u] LT","[pro\u0161le] [subote] [u] LT"][this.day()]},sameElse:"L"},relativeTime:{future:"za %s",past:"pre %s",s:"nekoliko sekundi",ss:$n.translate,m:$n.translate,mm:$n.translate,h:$n.translate,hh:$n.translate,d:"dan",dd:$n.translate,M:"mesec",MM:$n.translate,y:"godinu",yy:$n.translate},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:7}}),e.defineLocale("ss",{months:"Bhimbidvwane_Indlovana_Indlov'lenkhulu_Mabasa_Inkhwekhweti_Inhlaba_Kholwane_Ingci_Inyoni_Imphala_Lweti_Ingongoni".split("_"),monthsShort:"Bhi_Ina_Inu_Mab_Ink_Inh_Kho_Igc_Iny_Imp_Lwe_Igo".split("_"),weekdays:"Lisontfo_Umsombuluko_Lesibili_Lesitsatfu_Lesine_Lesihlanu_Umgcibelo".split("_"),weekdaysShort:"Lis_Umb_Lsb_Les_Lsi_Lsh_Umg".split("_"),weekdaysMin:"Li_Us_Lb_Lt_Ls_Lh_Ug".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[Namuhla nga] LT",nextDay:"[Kusasa nga] LT",nextWeek:"dddd [nga] LT",lastDay:"[Itolo nga] LT",lastWeek:"dddd [leliphelile] [nga] LT",sameElse:"L"},relativeTime:{future:"nga %s",past:"wenteka nga %s",s:"emizuzwana lomcane",ss:"%d mzuzwana",m:"umzuzu",mm:"%d emizuzu",h:"lihora",hh:"%d emahora",d:"lilanga",dd:"%d emalanga",M:"inyanga",MM:"%d tinyanga",y:"umnyaka",yy:"%d iminyaka"},meridiemParse:/ekuseni|emini|entsambama|ebusuku/,meridiem:function(e,a,t){return e<11?"ekuseni":e<15?"emini":e<19?"entsambama":"ebusuku"},meridiemHour:function(e,a){return 12===e&&(e=0),"ekuseni"===a?e:"emini"===a?e>=11?e:e+12:"entsambama"===a||"ebusuku"===a?0===e?0:e+12:void 0},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:"%d",week:{dow:1,doy:4}}),e.defineLocale("sv",{months:"januari_februari_mars_april_maj_juni_juli_augusti_september_oktober_november_december".split("_"),monthsShort:"jan_feb_mar_apr_maj_jun_jul_aug_sep_okt_nov_dec".split("_"),weekdays:"s\xf6ndag_m\xe5ndag_tisdag_onsdag_torsdag_fredag_l\xf6rdag".split("_"),weekdaysShort:"s\xf6n_m\xe5n_tis_ons_tor_fre_l\xf6r".split("_"),weekdaysMin:"s\xf6_m\xe5_ti_on_to_fr_l\xf6".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY-MM-DD",LL:"D MMMM YYYY",LLL:"D MMMM YYYY [kl.] HH:mm",LLLL:"dddd D MMMM YYYY [kl.] HH:mm",lll:"D MMM YYYY HH:mm",llll:"ddd D MMM YYYY HH:mm"},calendar:{sameDay:"[Idag] LT",nextDay:"[Imorgon] LT",lastDay:"[Ig\xe5r] LT",nextWeek:"[P\xe5] dddd LT",lastWeek:"[I] dddd[s] LT",sameElse:"L"},relativeTime:{future:"om %s",past:"f\xf6r %s sedan",s:"n\xe5gra sekunder",ss:"%d sekunder",m:"en minut",mm:"%d minuter",h:"en timme",hh:"%d timmar",d:"en dag",dd:"%d dagar",M:"en m\xe5nad",MM:"%d m\xe5nader",y:"ett \xe5r",yy:"%d \xe5r"},dayOfMonthOrdinalParse:/\d{1,2}(e|a)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"e":1===a?"a":2===a?"a":"e")},week:{dow:1,doy:4}}),e.defineLocale("sw",{months:"Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba".split("_"),monthsShort:"Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des".split("_"),weekdays:"Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi".split("_"),weekdaysShort:"Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos".split("_"),weekdaysMin:"J2_J3_J4_J5_Al_Ij_J1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[leo saa] LT",nextDay:"[kesho saa] LT",nextWeek:"[wiki ijayo] dddd [saat] LT",lastDay:"[jana] LT",lastWeek:"[wiki iliyopita] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s baadaye",past:"tokea %s",s:"hivi punde",ss:"sekunde %d",m:"dakika moja",mm:"dakika %d",h:"saa limoja",hh:"masaa %d",d:"siku moja",dd:"masiku %d",M:"mwezi mmoja",MM:"miezi %d",y:"mwaka mmoja",yy:"miaka %d"},week:{dow:1,doy:7}});var Bn={1:"\u0be7",2:"\u0be8",3:"\u0be9",4:"\u0bea",5:"\u0beb",6:"\u0bec",7:"\u0bed",8:"\u0bee",9:"\u0bef",0:"\u0be6"},qn={"\u0be7":"1","\u0be8":"2","\u0be9":"3","\u0bea":"4","\u0beb":"5","\u0bec":"6","\u0bed":"7","\u0bee":"8","\u0bef":"9","\u0be6":"0"};e.defineLocale("ta",{months:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),monthsShort:"\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf_\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf_\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd_\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd_\u0bae\u0bc7_\u0b9c\u0bc2\u0ba9\u0bcd_\u0b9c\u0bc2\u0bb2\u0bc8_\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd_\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bc6\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b85\u0b95\u0bcd\u0b9f\u0bc7\u0bbe\u0baa\u0bb0\u0bcd_\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd_\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd".split("_"),weekdays:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bcd\u0bb1\u0bc1\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0b9f\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8_\u0b9a\u0ba9\u0bbf\u0b95\u0bcd\u0b95\u0bbf\u0bb4\u0bae\u0bc8".split("_"),weekdaysShort:"\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1_\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd_\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd_\u0baa\u0bc1\u0ba4\u0ba9\u0bcd_\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0ba9\u0bcd_\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf_\u0b9a\u0ba9\u0bbf".split("_"),weekdaysMin:"\u0b9e\u0bbe_\u0ba4\u0bbf_\u0b9a\u0bc6_\u0baa\u0bc1_\u0bb5\u0bbf_\u0bb5\u0bc6_\u0b9a".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, HH:mm",LLLL:"dddd, D MMMM YYYY, HH:mm"},calendar:{sameDay:"[\u0b87\u0ba9\u0bcd\u0bb1\u0bc1] LT",nextDay:"[\u0ba8\u0bbe\u0bb3\u0bc8] LT",nextWeek:"dddd, LT",lastDay:"[\u0ba8\u0bc7\u0bb1\u0bcd\u0bb1\u0bc1] LT",lastWeek:"[\u0b95\u0b9f\u0ba8\u0bcd\u0ba4 \u0bb5\u0bbe\u0bb0\u0bae\u0bcd] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0b87\u0bb2\u0bcd",past:"%s \u0bae\u0bc1\u0ba9\u0bcd",s:"\u0b92\u0bb0\u0bc1 \u0b9a\u0bbf\u0bb2 \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",ss:"%d \u0bb5\u0bbf\u0ba8\u0bbe\u0b9f\u0bbf\u0b95\u0bb3\u0bcd",m:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0bae\u0bcd",mm:"%d \u0ba8\u0bbf\u0bae\u0bbf\u0b9f\u0b99\u0bcd\u0b95\u0bb3\u0bcd",h:"\u0b92\u0bb0\u0bc1 \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",hh:"%d \u0bae\u0ba3\u0bbf \u0ba8\u0bc7\u0bb0\u0bae\u0bcd",d:"\u0b92\u0bb0\u0bc1 \u0ba8\u0bbe\u0bb3\u0bcd",dd:"%d \u0ba8\u0bbe\u0b9f\u0bcd\u0b95\u0bb3\u0bcd",M:"\u0b92\u0bb0\u0bc1 \u0bae\u0bbe\u0ba4\u0bae\u0bcd",MM:"%d \u0bae\u0bbe\u0ba4\u0b99\u0bcd\u0b95\u0bb3\u0bcd",y:"\u0b92\u0bb0\u0bc1 \u0bb5\u0bb0\u0bc1\u0b9f\u0bae\u0bcd",yy:"%d \u0b86\u0ba3\u0bcd\u0b9f\u0bc1\u0b95\u0bb3\u0bcd"},dayOfMonthOrdinalParse:/\d{1,2}\u0bb5\u0ba4\u0bc1/,ordinal:function(e){return e+"\u0bb5\u0ba4\u0bc1"},preparse:function(e){return e.replace(/[\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0be6]/g,function(e){return qn[e]})},postformat:function(e){return e.replace(/\d/g,function(e){return Bn[e]})},meridiemParse:/\u0baf\u0bbe\u0bae\u0bae\u0bcd|\u0bb5\u0bc8\u0b95\u0bb1\u0bc8|\u0b95\u0bbe\u0bb2\u0bc8|\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd|\u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1|\u0bae\u0bbe\u0bb2\u0bc8/,meridiem:function(e,a,t){return e<2?" \u0baf\u0bbe\u0bae\u0bae\u0bcd":e<6?" \u0bb5\u0bc8\u0b95\u0bb1\u0bc8":e<10?" \u0b95\u0bbe\u0bb2\u0bc8":e<14?" \u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd":e<18?" \u0b8e\u0bb1\u0bcd\u0baa\u0bbe\u0b9f\u0bc1":e<22?" \u0bae\u0bbe\u0bb2\u0bc8":" \u0baf\u0bbe\u0bae\u0bae\u0bcd"},meridiemHour:function(e,a){return 12===e&&(e=0),"\u0baf\u0bbe\u0bae\u0bae\u0bcd"===a?e<2?e:e+12:"\u0bb5\u0bc8\u0b95\u0bb1\u0bc8"===a||"\u0b95\u0bbe\u0bb2\u0bc8"===a?e:"\u0ba8\u0ba3\u0bcd\u0baa\u0b95\u0bb2\u0bcd"===a&&e>=10?e:e+12},week:{dow:0,doy:6}}),e.defineLocale("te",{months:"\u0c1c\u0c28\u0c35\u0c30\u0c3f_\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f_\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d_\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c42\u0c32\u0c46\u0c56_\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41_\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d_\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d_\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d_\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d".split("_"),monthsShort:"\u0c1c\u0c28._\u0c2b\u0c3f\u0c2c\u0c4d\u0c30._\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f_\u0c0f\u0c2a\u0c4d\u0c30\u0c3f._\u0c2e\u0c47_\u0c1c\u0c42\u0c28\u0c4d_\u0c1c\u0c42\u0c32\u0c46\u0c56_\u0c06\u0c17._\u0c38\u0c46\u0c2a\u0c4d._\u0c05\u0c15\u0c4d\u0c1f\u0c4b._\u0c28\u0c35._\u0c21\u0c3f\u0c38\u0c46.".split("_"),monthsParseExact:!0,weekdays:"\u0c06\u0c26\u0c3f\u0c35\u0c3e\u0c30\u0c02_\u0c38\u0c4b\u0c2e\u0c35\u0c3e\u0c30\u0c02_\u0c2e\u0c02\u0c17\u0c33\u0c35\u0c3e\u0c30\u0c02_\u0c2c\u0c41\u0c27\u0c35\u0c3e\u0c30\u0c02_\u0c17\u0c41\u0c30\u0c41\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c41\u0c15\u0c4d\u0c30\u0c35\u0c3e\u0c30\u0c02_\u0c36\u0c28\u0c3f\u0c35\u0c3e\u0c30\u0c02".split("_"),weekdaysShort:"\u0c06\u0c26\u0c3f_\u0c38\u0c4b\u0c2e_\u0c2e\u0c02\u0c17\u0c33_\u0c2c\u0c41\u0c27_\u0c17\u0c41\u0c30\u0c41_\u0c36\u0c41\u0c15\u0c4d\u0c30_\u0c36\u0c28\u0c3f".split("_"),weekdaysMin:"\u0c06_\u0c38\u0c4b_\u0c2e\u0c02_\u0c2c\u0c41_\u0c17\u0c41_\u0c36\u0c41_\u0c36".split("_"),longDateFormat:{LT:"A h:mm",LTS:"A h:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY, A h:mm",LLLL:"dddd, D MMMM YYYY, A h:mm"},calendar:{sameDay:"[\u0c28\u0c47\u0c21\u0c41] LT",nextDay:"[\u0c30\u0c47\u0c2a\u0c41] LT",nextWeek:"dddd, LT",lastDay:"[\u0c28\u0c3f\u0c28\u0c4d\u0c28] LT",lastWeek:"[\u0c17\u0c24] dddd, LT",sameElse:"L"},relativeTime:{future:"%s \u0c32\u0c4b",past:"%s \u0c15\u0c4d\u0c30\u0c3f\u0c24\u0c02",s:"\u0c15\u0c4a\u0c28\u0c4d\u0c28\u0c3f \u0c15\u0c4d\u0c37\u0c23\u0c3e\u0c32\u0c41",ss:"%d \u0c38\u0c46\u0c15\u0c28\u0c4d\u0c32\u0c41",m:"\u0c12\u0c15 \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c02",mm:"%d \u0c28\u0c3f\u0c2e\u0c3f\u0c37\u0c3e\u0c32\u0c41",h:"\u0c12\u0c15 \u0c17\u0c02\u0c1f",hh:"%d \u0c17\u0c02\u0c1f\u0c32\u0c41",d:"\u0c12\u0c15 \u0c30\u0c4b\u0c1c\u0c41",dd:"%d \u0c30\u0c4b\u0c1c\u0c41\u0c32\u0c41",M:"\u0c12\u0c15 \u0c28\u0c46\u0c32",MM:"%d \u0c28\u0c46\u0c32\u0c32\u0c41",y:"\u0c12\u0c15 \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c02",yy:"%d \u0c38\u0c02\u0c35\u0c24\u0c4d\u0c38\u0c30\u0c3e\u0c32\u0c41"},dayOfMonthOrdinalParse:/\d{1,2}\u0c35/,ordinal:"%d\u0c35",meridiemParse:/\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f|\u0c09\u0c26\u0c2f\u0c02|\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02|\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"===a?e<4?e:e+12:"\u0c09\u0c26\u0c2f\u0c02"===a?e:"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02"===a?e>=10?e:e+12:"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02"===a?e+12:void 0},meridiem:function(e,a,t){return e<4?"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f":e<10?"\u0c09\u0c26\u0c2f\u0c02":e<17?"\u0c2e\u0c27\u0c4d\u0c2f\u0c3e\u0c39\u0c4d\u0c28\u0c02":e<20?"\u0c38\u0c3e\u0c2f\u0c02\u0c24\u0c4d\u0c30\u0c02":"\u0c30\u0c3e\u0c24\u0c4d\u0c30\u0c3f"},week:{dow:0,doy:6}}),e.defineLocale("tet",{months:"Janeiru_Fevereiru_Marsu_Abril_Maiu_Juniu_Juliu_Augustu_Setembru_Outubru_Novembru_Dezembru".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Aug_Set_Out_Nov_Dez".split("_"),weekdays:"Domingu_Segunda_Tersa_Kuarta_Kinta_Sexta_Sabadu".split("_"),weekdaysShort:"Dom_Seg_Ters_Kua_Kint_Sext_Sab".split("_"),weekdaysMin:"Do_Seg_Te_Ku_Ki_Sex_Sa".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[Ohin iha] LT",nextDay:"[Aban iha] LT",nextWeek:"dddd [iha] LT",lastDay:"[Horiseik iha] LT",lastWeek:"dddd [semana kotuk] [iha] LT",sameElse:"L"},relativeTime:{future:"iha %s",past:"%s liuba",s:"minutu balun",ss:"minutu %d",m:"minutu ida",mm:"minutus %d",h:"horas ida",hh:"horas %d",d:"loron ida",dd:"loron %d",M:"fulan ida",MM:"fulan %d",y:"tinan ida",yy:"tinan %d"},dayOfMonthOrdinalParse:/\d{1,2}(st|nd|rd|th)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("th",{months:"\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21_\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c_\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21_\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19_\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21_\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19_\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21_\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21_\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19_\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21_\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19_\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21".split("_"),monthsShort:"\u0e21.\u0e04._\u0e01.\u0e1e._\u0e21\u0e35.\u0e04._\u0e40\u0e21.\u0e22._\u0e1e.\u0e04._\u0e21\u0e34.\u0e22._\u0e01.\u0e04._\u0e2a.\u0e04._\u0e01.\u0e22._\u0e15.\u0e04._\u0e1e.\u0e22._\u0e18.\u0e04.".split("_"),monthsParseExact:!0,weekdays:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysShort:"\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c_\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c_\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23_\u0e1e\u0e38\u0e18_\u0e1e\u0e24\u0e2b\u0e31\u0e2a_\u0e28\u0e38\u0e01\u0e23\u0e4c_\u0e40\u0e2a\u0e32\u0e23\u0e4c".split("_"),weekdaysMin:"\u0e2d\u0e32._\u0e08._\u0e2d._\u0e1e._\u0e1e\u0e24._\u0e28._\u0e2a.".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm",LLLL:"\u0e27\u0e31\u0e19dddd\u0e17\u0e35\u0e48 D MMMM YYYY \u0e40\u0e27\u0e25\u0e32 H:mm"},meridiemParse:/\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07|\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07/,isPM:function(e){return"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"===e},meridiem:function(e,a,t){return e<12?"\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07":"\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07"},calendar:{sameDay:"[\u0e27\u0e31\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextDay:"[\u0e1e\u0e23\u0e38\u0e48\u0e07\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",nextWeek:"dddd[\u0e2b\u0e19\u0e49\u0e32 \u0e40\u0e27\u0e25\u0e32] LT",lastDay:"[\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e32\u0e19\u0e19\u0e35\u0e49 \u0e40\u0e27\u0e25\u0e32] LT",lastWeek:"[\u0e27\u0e31\u0e19]dddd[\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27 \u0e40\u0e27\u0e25\u0e32] LT",sameElse:"L"},relativeTime:{future:"\u0e2d\u0e35\u0e01 %s",past:"%s\u0e17\u0e35\u0e48\u0e41\u0e25\u0e49\u0e27",s:"\u0e44\u0e21\u0e48\u0e01\u0e35\u0e48\u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",ss:"%d \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35",m:"1 \u0e19\u0e32\u0e17\u0e35",mm:"%d \u0e19\u0e32\u0e17\u0e35",h:"1 \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",hh:"%d \u0e0a\u0e31\u0e48\u0e27\u0e42\u0e21\u0e07",d:"1 \u0e27\u0e31\u0e19",dd:"%d \u0e27\u0e31\u0e19",M:"1 \u0e40\u0e14\u0e37\u0e2d\u0e19",MM:"%d \u0e40\u0e14\u0e37\u0e2d\u0e19",y:"1 \u0e1b\u0e35",yy:"%d \u0e1b\u0e35"}}),e.defineLocale("tl-ph",{months:"Enero_Pebrero_Marso_Abril_Mayo_Hunyo_Hulyo_Agosto_Setyembre_Oktubre_Nobyembre_Disyembre".split("_"),monthsShort:"Ene_Peb_Mar_Abr_May_Hun_Hul_Ago_Set_Okt_Nob_Dis".split("_"),weekdays:"Linggo_Lunes_Martes_Miyerkules_Huwebes_Biyernes_Sabado".split("_"),weekdaysShort:"Lin_Lun_Mar_Miy_Huw_Biy_Sab".split("_"),weekdaysMin:"Li_Lu_Ma_Mi_Hu_Bi_Sab".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"MM/D/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY HH:mm",LLLL:"dddd, MMMM DD, YYYY HH:mm"},calendar:{sameDay:"LT [ngayong araw]",nextDay:"[Bukas ng] LT",nextWeek:"LT [sa susunod na] dddd",lastDay:"LT [kahapon]",lastWeek:"LT [noong nakaraang] dddd",sameElse:"L"},relativeTime:{future:"sa loob ng %s",past:"%s ang nakalipas",s:"ilang segundo",ss:"%d segundo",m:"isang minuto",mm:"%d minuto",h:"isang oras",hh:"%d oras",d:"isang araw",dd:"%d araw",M:"isang buwan",MM:"%d buwan",y:"isang taon",yy:"%d taon"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}});var Qn="pagh_wa\u2019_cha\u2019_wej_loS_vagh_jav_Soch_chorgh_Hut".split("_");e.defineLocale("tlh",{months:"tera\u2019 jar wa\u2019_tera\u2019 jar cha\u2019_tera\u2019 jar wej_tera\u2019 jar loS_tera\u2019 jar vagh_tera\u2019 jar jav_tera\u2019 jar Soch_tera\u2019 jar chorgh_tera\u2019 jar Hut_tera\u2019 jar wa\u2019maH_tera\u2019 jar wa\u2019maH wa\u2019_tera\u2019 jar wa\u2019maH cha\u2019".split("_"),monthsShort:"jar wa\u2019_jar cha\u2019_jar wej_jar loS_jar vagh_jar jav_jar Soch_jar chorgh_jar Hut_jar wa\u2019maH_jar wa\u2019maH wa\u2019_jar wa\u2019maH cha\u2019".split("_"),monthsParseExact:!0,weekdays:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysShort:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),weekdaysMin:"lojmItjaj_DaSjaj_povjaj_ghItlhjaj_loghjaj_buqjaj_ghInjaj".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[DaHjaj] LT",nextDay:"[wa\u2019leS] LT",nextWeek:"LLL",lastDay:"[wa\u2019Hu\u2019] LT",lastWeek:"LLL",sameElse:"L"},relativeTime:{future:function(e){var a=e;return a=-1!==e.indexOf("jaj")?a.slice(0,-3)+"leS":-1!==e.indexOf("jar")?a.slice(0,-3)+"waQ":-1!==e.indexOf("DIS")?a.slice(0,-3)+"nem":a+" pIq"},past:function(e){var a=e;return a=-1!==e.indexOf("jaj")?a.slice(0,-3)+"Hu\u2019":-1!==e.indexOf("jar")?a.slice(0,-3)+"wen":-1!==e.indexOf("DIS")?a.slice(0,-3)+"ben":a+" ret"},s:"puS lup",ss:xa,m:"wa\u2019 tup",mm:xa,h:"wa\u2019 rep",hh:xa,d:"wa\u2019 jaj",dd:xa,M:"wa\u2019 jar",MM:xa,y:"wa\u2019 DIS",yy:xa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}});var Xn={1:"'inci",5:"'inci",8:"'inci",70:"'inci",80:"'inci",2:"'nci",7:"'nci",20:"'nci",50:"'nci",3:"'\xfcnc\xfc",4:"'\xfcnc\xfc",100:"'\xfcnc\xfc",6:"'nc\u0131",9:"'uncu",10:"'uncu",30:"'uncu",60:"'\u0131nc\u0131",90:"'\u0131nc\u0131"};e.defineLocale("tr",{months:"Ocak_\u015eubat_Mart_Nisan_May\u0131s_Haziran_Temmuz_A\u011fustos_Eyl\xfcl_Ekim_Kas\u0131m_Aral\u0131k".split("_"),monthsShort:"Oca_\u015eub_Mar_Nis_May_Haz_Tem_A\u011fu_Eyl_Eki_Kas_Ara".split("_"),weekdays:"Pazar_Pazartesi_Sal\u0131_\xc7ar\u015famba_Per\u015fembe_Cuma_Cumartesi".split("_"),weekdaysShort:"Paz_Pts_Sal_\xc7ar_Per_Cum_Cts".split("_"),weekdaysMin:"Pz_Pt_Sa_\xc7a_Pe_Cu_Ct".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[bug\xfcn saat] LT",nextDay:"[yar\u0131n saat] LT",nextWeek:"[gelecek] dddd [saat] LT",lastDay:"[d\xfcn] LT",lastWeek:"[ge\xe7en] dddd [saat] LT",sameElse:"L"},relativeTime:{future:"%s sonra",past:"%s \xf6nce",s:"birka\xe7 saniye",ss:"%d saniye",m:"bir dakika",mm:"%d dakika",h:"bir saat",hh:"%d saat",d:"bir g\xfcn",dd:"%d g\xfcn",M:"bir ay",MM:"%d ay",y:"bir y\u0131l",yy:"%d y\u0131l"},dayOfMonthOrdinalParse:/\d{1,2}'(inci|nci|\xfcnc\xfc|nc\u0131|uncu|\u0131nc\u0131)/,ordinal:function(e){if(0===e)return e+"'\u0131nc\u0131";var a=e%10;return e+(Xn[a]||Xn[e%100-a]||Xn[e>=100?100:null])},week:{dow:1,doy:7}}),e.defineLocale("tzl",{months:"Januar_Fevraglh_Mar\xe7_Avr\xefu_Mai_G\xfcn_Julia_Guscht_Setemvar_Listop\xe4ts_Noemvar_Zecemvar".split("_"),monthsShort:"Jan_Fev_Mar_Avr_Mai_G\xfcn_Jul_Gus_Set_Lis_Noe_Zec".split("_"),weekdays:"S\xfaladi_L\xfane\xe7i_Maitzi_M\xe1rcuri_Xh\xfaadi_Vi\xe9ner\xe7i_S\xe1turi".split("_"),weekdaysShort:"S\xfal_L\xfan_Mai_M\xe1r_Xh\xfa_Vi\xe9_S\xe1t".split("_"),weekdaysMin:"S\xfa_L\xfa_Ma_M\xe1_Xh_Vi_S\xe1".split("_"),longDateFormat:{LT:"HH.mm",LTS:"HH.mm.ss",L:"DD.MM.YYYY",LL:"D. MMMM [dallas] YYYY",LLL:"D. MMMM [dallas] YYYY HH.mm",LLLL:"dddd, [li] D. MMMM [dallas] YYYY HH.mm"},meridiemParse:/d\'o|d\'a/i,isPM:function(e){return"d'o"===e.toLowerCase()},meridiem:function(e,a,t){return e>11?t?"d'o":"D'O":t?"d'a":"D'A"},calendar:{sameDay:"[oxhi \xe0] LT",nextDay:"[dem\xe0 \xe0] LT",nextWeek:"dddd [\xe0] LT",lastDay:"[ieiri \xe0] LT",lastWeek:"[s\xfcr el] dddd [lasteu \xe0] LT",sameElse:"L"},relativeTime:{future:"osprei %s",past:"ja%s",s:Pa,ss:Pa,m:Pa,mm:Pa,h:Pa,hh:Pa,d:Pa,dd:Pa,M:Pa,MM:Pa,y:Pa,yy:Pa},dayOfMonthOrdinalParse:/\d{1,2}\./,ordinal:"%d.",week:{dow:1,doy:4}}),e.defineLocale("tzm-latn",{months:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),monthsShort:"innayr_br\u02e4ayr\u02e4_mar\u02e4s\u02e4_ibrir_mayyw_ywnyw_ywlywz_\u0263w\u0161t_\u0161wtanbir_kt\u02e4wbr\u02e4_nwwanbir_dwjnbir".split("_"),weekdays:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysShort:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),weekdaysMin:"asamas_aynas_asinas_akras_akwas_asimwas_asi\u1e0dyas".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[asdkh g] LT",nextDay:"[aska g] LT",nextWeek:"dddd [g] LT",lastDay:"[assant g] LT",lastWeek:"dddd [g] LT",sameElse:"L"},relativeTime:{future:"dadkh s yan %s",past:"yan %s",s:"imik",ss:"%d imik",m:"minu\u1e0d",mm:"%d minu\u1e0d",h:"sa\u025ba",hh:"%d tassa\u025bin",d:"ass",dd:"%d ossan",M:"ayowr",MM:"%d iyyirn",y:"asgas",yy:"%d isgasn"},week:{dow:6,doy:12}}),e.defineLocale("tzm",{months:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),monthsShort:"\u2d49\u2d4f\u2d4f\u2d30\u2d62\u2d54_\u2d31\u2d55\u2d30\u2d62\u2d55_\u2d4e\u2d30\u2d55\u2d5a_\u2d49\u2d31\u2d54\u2d49\u2d54_\u2d4e\u2d30\u2d62\u2d62\u2d53_\u2d62\u2d53\u2d4f\u2d62\u2d53_\u2d62\u2d53\u2d4d\u2d62\u2d53\u2d63_\u2d56\u2d53\u2d5b\u2d5c_\u2d5b\u2d53\u2d5c\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d3d\u2d5f\u2d53\u2d31\u2d55_\u2d4f\u2d53\u2d61\u2d30\u2d4f\u2d31\u2d49\u2d54_\u2d37\u2d53\u2d4a\u2d4f\u2d31\u2d49\u2d54".split("_"),weekdays:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysShort:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),weekdaysMin:"\u2d30\u2d59\u2d30\u2d4e\u2d30\u2d59_\u2d30\u2d62\u2d4f\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4f\u2d30\u2d59_\u2d30\u2d3d\u2d54\u2d30\u2d59_\u2d30\u2d3d\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d4e\u2d61\u2d30\u2d59_\u2d30\u2d59\u2d49\u2d39\u2d62\u2d30\u2d59".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},calendar:{sameDay:"[\u2d30\u2d59\u2d37\u2d45 \u2d34] LT",nextDay:"[\u2d30\u2d59\u2d3d\u2d30 \u2d34] LT",nextWeek:"dddd [\u2d34] LT",lastDay:"[\u2d30\u2d5a\u2d30\u2d4f\u2d5c \u2d34] LT",lastWeek:"dddd [\u2d34] LT",sameElse:"L"},relativeTime:{future:"\u2d37\u2d30\u2d37\u2d45 \u2d59 \u2d62\u2d30\u2d4f %s",past:"\u2d62\u2d30\u2d4f %s",s:"\u2d49\u2d4e\u2d49\u2d3d",ss:"%d \u2d49\u2d4e\u2d49\u2d3d",m:"\u2d4e\u2d49\u2d4f\u2d53\u2d3a",mm:"%d \u2d4e\u2d49\u2d4f\u2d53\u2d3a",h:"\u2d59\u2d30\u2d44\u2d30",hh:"%d \u2d5c\u2d30\u2d59\u2d59\u2d30\u2d44\u2d49\u2d4f",d:"\u2d30\u2d59\u2d59",dd:"%d o\u2d59\u2d59\u2d30\u2d4f",M:"\u2d30\u2d62o\u2d53\u2d54",MM:"%d \u2d49\u2d62\u2d62\u2d49\u2d54\u2d4f",y:"\u2d30\u2d59\u2d33\u2d30\u2d59",yy:"%d \u2d49\u2d59\u2d33\u2d30\u2d59\u2d4f"},week:{dow:6,doy:12}}),e.defineLocale("uk",{months:{format:"\u0441\u0456\u0447\u043d\u044f_\u043b\u044e\u0442\u043e\u0433\u043e_\u0431\u0435\u0440\u0435\u0437\u043d\u044f_\u043a\u0432\u0456\u0442\u043d\u044f_\u0442\u0440\u0430\u0432\u043d\u044f_\u0447\u0435\u0440\u0432\u043d\u044f_\u043b\u0438\u043f\u043d\u044f_\u0441\u0435\u0440\u043f\u043d\u044f_\u0432\u0435\u0440\u0435\u0441\u043d\u044f_\u0436\u043e\u0432\u0442\u043d\u044f_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430_\u0433\u0440\u0443\u0434\u043d\u044f".split("_"),standalone:"\u0441\u0456\u0447\u0435\u043d\u044c_\u043b\u044e\u0442\u0438\u0439_\u0431\u0435\u0440\u0435\u0437\u0435\u043d\u044c_\u043a\u0432\u0456\u0442\u0435\u043d\u044c_\u0442\u0440\u0430\u0432\u0435\u043d\u044c_\u0447\u0435\u0440\u0432\u0435\u043d\u044c_\u043b\u0438\u043f\u0435\u043d\u044c_\u0441\u0435\u0440\u043f\u0435\u043d\u044c_\u0432\u0435\u0440\u0435\u0441\u0435\u043d\u044c_\u0436\u043e\u0432\u0442\u0435\u043d\u044c_\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434_\u0433\u0440\u0443\u0434\u0435\u043d\u044c".split("_")},monthsShort:"\u0441\u0456\u0447_\u043b\u044e\u0442_\u0431\u0435\u0440_\u043a\u0432\u0456\u0442_\u0442\u0440\u0430\u0432_\u0447\u0435\u0440\u0432_\u043b\u0438\u043f_\u0441\u0435\u0440\u043f_\u0432\u0435\u0440_\u0436\u043e\u0432\u0442_\u043b\u0438\u0441\u0442_\u0433\u0440\u0443\u0434".split("_"),weekdays:function(e,a){var t={nominative:"\u043d\u0435\u0434\u0456\u043b\u044f_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0430_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044f_\u0441\u0443\u0431\u043e\u0442\u0430".split("_"),accusative:"\u043d\u0435\u0434\u0456\u043b\u044e_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a_\u0432\u0456\u0432\u0442\u043e\u0440\u043e\u043a_\u0441\u0435\u0440\u0435\u0434\u0443_\u0447\u0435\u0442\u0432\u0435\u0440_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u044e_\u0441\u0443\u0431\u043e\u0442\u0443".split("_"),genitive:"\u043d\u0435\u0434\u0456\u043b\u0456_\u043f\u043e\u043d\u0435\u0434\u0456\u043b\u043a\u0430_\u0432\u0456\u0432\u0442\u043e\u0440\u043a\u0430_\u0441\u0435\u0440\u0435\u0434\u0438_\u0447\u0435\u0442\u0432\u0435\u0440\u0433\u0430_\u043f\u2019\u044f\u0442\u043d\u0438\u0446\u0456_\u0441\u0443\u0431\u043e\u0442\u0438".split("_")};return e?t[/(\[[\u0412\u0432\u0423\u0443]\]) ?dddd/.test(a)?"accusative":/\[?(?:\u043c\u0438\u043d\u0443\u043b\u043e\u0457|\u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0457)? ?\] ?dddd/.test(a)?"genitive":"nominative"][e.day()]:t.nominative},weekdaysShort:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),weekdaysMin:"\u043d\u0434_\u043f\u043d_\u0432\u0442_\u0441\u0440_\u0447\u0442_\u043f\u0442_\u0441\u0431".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD.MM.YYYY",LL:"D MMMM YYYY \u0440.",LLL:"D MMMM YYYY \u0440., HH:mm",LLLL:"dddd, D MMMM YYYY \u0440., HH:mm"},calendar:{sameDay:Wa("[\u0421\u044c\u043e\u0433\u043e\u0434\u043d\u0456 "),nextDay:Wa("[\u0417\u0430\u0432\u0442\u0440\u0430 "),lastDay:Wa("[\u0412\u0447\u043e\u0440\u0430 "),nextWeek:Wa("[\u0423] dddd ["),lastWeek:function(){switch(this.day()){case 0:case 3:case 5:case 6:return Wa("[\u041c\u0438\u043d\u0443\u043b\u043e\u0457] dddd [").call(this);case 1:case 2:case 4:return Wa("[\u041c\u0438\u043d\u0443\u043b\u043e\u0433\u043e] dddd [").call(this)}},sameElse:"L"},relativeTime:{future:"\u0437\u0430 %s",past:"%s \u0442\u043e\u043c\u0443",s:"\u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430 \u0441\u0435\u043a\u0443\u043d\u0434",ss:Oa,m:Oa,mm:Oa,h:"\u0433\u043e\u0434\u0438\u043d\u0443",hh:Oa,d:"\u0434\u0435\u043d\u044c",dd:Oa,M:"\u043c\u0456\u0441\u044f\u0446\u044c",MM:Oa,y:"\u0440\u0456\u043a",yy:Oa},meridiemParse:/\u043d\u043e\u0447\u0456|\u0440\u0430\u043d\u043a\u0443|\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430/,isPM:function(e){return/^(\u0434\u043d\u044f|\u0432\u0435\u0447\u043e\u0440\u0430)$/.test(e)},meridiem:function(e,a,t){return e<4?"\u043d\u043e\u0447\u0456":e<12?"\u0440\u0430\u043d\u043a\u0443":e<17?"\u0434\u043d\u044f":"\u0432\u0435\u0447\u043e\u0440\u0430"},dayOfMonthOrdinalParse:/\d{1,2}-(\u0439|\u0433\u043e)/,ordinal:function(e,a){switch(a){case"M":case"d":case"DDD":case"w":case"W":return e+"-\u0439";case"D":return e+"-\u0433\u043e";default:return e}},week:{dow:1,doy:7}});var ed=["\u062c\u0646\u0648\u0631\u06cc","\u0641\u0631\u0648\u0631\u06cc","\u0645\u0627\u0631\u0686","\u0627\u067e\u0631\u06cc\u0644","\u0645\u0626\u06cc","\u062c\u0648\u0646","\u062c\u0648\u0644\u0627\u0626\u06cc","\u0627\u06af\u0633\u062a","\u0633\u062a\u0645\u0628\u0631","\u0627\u06a9\u062a\u0648\u0628\u0631","\u0646\u0648\u0645\u0628\u0631","\u062f\u0633\u0645\u0628\u0631"],ad=["\u0627\u062a\u0648\u0627\u0631","\u067e\u06cc\u0631","\u0645\u0646\u06af\u0644","\u0628\u062f\u06be","\u062c\u0645\u0639\u0631\u0627\u062a","\u062c\u0645\u0639\u06c1","\u06c1\u0641\u062a\u06c1"];return e.defineLocale("ur",{months:ed,monthsShort:ed,weekdays:ad,weekdaysShort:ad,weekdaysMin:ad,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd\u060c D MMMM YYYY HH:mm"},meridiemParse:/\u0635\u0628\u062d|\u0634\u0627\u0645/,isPM:function(e){return"\u0634\u0627\u0645"===e},meridiem:function(e,a,t){return e<12?"\u0635\u0628\u062d":"\u0634\u0627\u0645"},calendar:{sameDay:"[\u0622\u062c \u0628\u0648\u0642\u062a] LT",nextDay:"[\u06a9\u0644 \u0628\u0648\u0642\u062a] LT",nextWeek:"dddd [\u0628\u0648\u0642\u062a] LT",lastDay:"[\u06af\u0630\u0634\u062a\u06c1 \u0631\u0648\u0632 \u0628\u0648\u0642\u062a] LT",lastWeek:"[\u06af\u0630\u0634\u062a\u06c1] dddd [\u0628\u0648\u0642\u062a] LT",sameElse:"L"},relativeTime:{future:"%s \u0628\u0639\u062f",past:"%s \u0642\u0628\u0644",s:"\u0686\u0646\u062f \u0633\u06cc\u06a9\u0646\u0688",ss:"%d \u0633\u06cc\u06a9\u0646\u0688",m:"\u0627\u06cc\u06a9 \u0645\u0646\u0679",mm:"%d \u0645\u0646\u0679",h:"\u0627\u06cc\u06a9 \u06af\u06be\u0646\u0679\u06c1",hh:"%d \u06af\u06be\u0646\u0679\u06d2",d:"\u0627\u06cc\u06a9 \u062f\u0646",dd:"%d \u062f\u0646",M:"\u0627\u06cc\u06a9 \u0645\u0627\u06c1",MM:"%d \u0645\u0627\u06c1",y:"\u0627\u06cc\u06a9 \u0633\u0627\u0644",yy:"%d \u0633\u0627\u0644"},preparse:function(e){return e.replace(/\u060c/g,",")},postformat:function(e){return e.replace(/,/g,"\u060c")},week:{dow:1,doy:4}}),e.defineLocale("uz-latn",{months:"Yanvar_Fevral_Mart_Aprel_May_Iyun_Iyul_Avgust_Sentabr_Oktabr_Noyabr_Dekabr".split("_"),monthsShort:"Yan_Fev_Mar_Apr_May_Iyun_Iyul_Avg_Sen_Okt_Noy_Dek".split("_"),weekdays:"Yakshanba_Dushanba_Seshanba_Chorshanba_Payshanba_Juma_Shanba".split("_"),weekdaysShort:"Yak_Dush_Sesh_Chor_Pay_Jum_Shan".split("_"),weekdaysMin:"Ya_Du_Se_Cho_Pa_Ju_Sha".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[Bugun soat] LT [da]",nextDay:"[Ertaga] LT [da]",nextWeek:"dddd [kuni soat] LT [da]",lastDay:"[Kecha soat] LT [da]",lastWeek:"[O'tgan] dddd [kuni soat] LT [da]",sameElse:"L"},relativeTime:{future:"Yaqin %s ichida",past:"Bir necha %s oldin",s:"soniya",ss:"%d soniya",m:"bir daqiqa",mm:"%d daqiqa",h:"bir soat",hh:"%d soat",d:"bir kun",dd:"%d kun",M:"bir oy",MM:"%d oy",y:"bir yil",yy:"%d yil"},week:{dow:1,doy:7}}),e.defineLocale("uz",{months:"\u044f\u043d\u0432\u0430\u0440_\u0444\u0435\u0432\u0440\u0430\u043b_\u043c\u0430\u0440\u0442_\u0430\u043f\u0440\u0435\u043b_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433\u0443\u0441\u0442_\u0441\u0435\u043d\u0442\u044f\u0431\u0440_\u043e\u043a\u0442\u044f\u0431\u0440_\u043d\u043e\u044f\u0431\u0440_\u0434\u0435\u043a\u0430\u0431\u0440".split("_"),monthsShort:"\u044f\u043d\u0432_\u0444\u0435\u0432_\u043c\u0430\u0440_\u0430\u043f\u0440_\u043c\u0430\u0439_\u0438\u044e\u043d_\u0438\u044e\u043b_\u0430\u0432\u0433_\u0441\u0435\u043d_\u043e\u043a\u0442_\u043d\u043e\u044f_\u0434\u0435\u043a".split("_"),weekdays:"\u042f\u043a\u0448\u0430\u043d\u0431\u0430_\u0414\u0443\u0448\u0430\u043d\u0431\u0430_\u0421\u0435\u0448\u0430\u043d\u0431\u0430_\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0430_\u041f\u0430\u0439\u0448\u0430\u043d\u0431\u0430_\u0416\u0443\u043c\u0430_\u0428\u0430\u043d\u0431\u0430".split("_"),weekdaysShort:"\u042f\u043a\u0448_\u0414\u0443\u0448_\u0421\u0435\u0448_\u0427\u043e\u0440_\u041f\u0430\u0439_\u0416\u0443\u043c_\u0428\u0430\u043d".split("_"),weekdaysMin:"\u042f\u043a_\u0414\u0443_\u0421\u0435_\u0427\u043e_\u041f\u0430_\u0416\u0443_\u0428\u0430".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"D MMMM YYYY, dddd HH:mm"},calendar:{sameDay:"[\u0411\u0443\u0433\u0443\u043d \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",nextDay:"[\u042d\u0440\u0442\u0430\u0433\u0430] LT [\u0434\u0430]",nextWeek:"dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastDay:"[\u041a\u0435\u0447\u0430 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",lastWeek:"[\u0423\u0442\u0433\u0430\u043d] dddd [\u043a\u0443\u043d\u0438 \u0441\u043e\u0430\u0442] LT [\u0434\u0430]",sameElse:"L"},relativeTime:{future:"\u042f\u043a\u0438\u043d %s \u0438\u0447\u0438\u0434\u0430",past:"\u0411\u0438\u0440 \u043d\u0435\u0447\u0430 %s \u043e\u043b\u0434\u0438\u043d",s:"\u0444\u0443\u0440\u0441\u0430\u0442",ss:"%d \u0444\u0443\u0440\u0441\u0430\u0442",m:"\u0431\u0438\u0440 \u0434\u0430\u043a\u0438\u043a\u0430",mm:"%d \u0434\u0430\u043a\u0438\u043a\u0430",h:"\u0431\u0438\u0440 \u0441\u043e\u0430\u0442",hh:"%d \u0441\u043e\u0430\u0442",d:"\u0431\u0438\u0440 \u043a\u0443\u043d",dd:"%d \u043a\u0443\u043d",M:"\u0431\u0438\u0440 \u043e\u0439",MM:"%d \u043e\u0439",y:"\u0431\u0438\u0440 \u0439\u0438\u043b",yy:"%d \u0439\u0438\u043b"},week:{dow:1,doy:7}}),e.defineLocale("vi",{months:"th\xe1ng 1_th\xe1ng 2_th\xe1ng 3_th\xe1ng 4_th\xe1ng 5_th\xe1ng 6_th\xe1ng 7_th\xe1ng 8_th\xe1ng 9_th\xe1ng 10_th\xe1ng 11_th\xe1ng 12".split("_"),monthsShort:"Th01_Th02_Th03_Th04_Th05_Th06_Th07_Th08_Th09_Th10_Th11_Th12".split("_"),monthsParseExact:!0,weekdays:"ch\u1ee7 nh\u1eadt_th\u1ee9 hai_th\u1ee9 ba_th\u1ee9 t\u01b0_th\u1ee9 n\u0103m_th\u1ee9 s\xe1u_th\u1ee9 b\u1ea3y".split("_"),weekdaysShort:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysMin:"CN_T2_T3_T4_T5_T6_T7".split("_"),weekdaysParseExact:!0,meridiemParse:/sa|ch/i,isPM:function(e){return/^ch$/i.test(e)},meridiem:function(e,a,t){return e<12?t?"sa":"SA":t?"ch":"CH"},longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM [n\u0103m] YYYY",LLL:"D MMMM [n\u0103m] YYYY HH:mm",LLLL:"dddd, D MMMM [n\u0103m] YYYY HH:mm",l:"DD/M/YYYY",ll:"D MMM YYYY",lll:"D MMM YYYY HH:mm",llll:"ddd, D MMM YYYY HH:mm"},calendar:{sameDay:"[H\xf4m nay l\xfac] LT",nextDay:"[Ng\xe0y mai l\xfac] LT",nextWeek:"dddd [tu\u1ea7n t\u1edbi l\xfac] LT",lastDay:"[H\xf4m qua l\xfac] LT",lastWeek:"dddd [tu\u1ea7n r\u1ed3i l\xfac] LT",sameElse:"L"},relativeTime:{future:"%s t\u1edbi",past:"%s tr\u01b0\u1edbc",s:"v\xe0i gi\xe2y",ss:"%d gi\xe2y",m:"m\u1ed9t ph\xfat",mm:"%d ph\xfat",h:"m\u1ed9t gi\u1edd",hh:"%d gi\u1edd",d:"m\u1ed9t ng\xe0y",dd:"%d ng\xe0y",M:"m\u1ed9t th\xe1ng",MM:"%d th\xe1ng",y:"m\u1ed9t n\u0103m",yy:"%d n\u0103m"},dayOfMonthOrdinalParse:/\d{1,2}/,ordinal:function(e){return e},week:{dow:1,doy:4}}),e.defineLocale("x-pseudo",{months:"J~\xe1\xf1\xfa\xe1~r\xfd_F~\xe9br\xfa~\xe1r\xfd_~M\xe1rc~h_\xc1p~r\xedl_~M\xe1\xfd_~J\xfa\xf1\xe9~_J\xfal~\xfd_\xc1\xfa~g\xfast~_S\xe9p~t\xe9mb~\xe9r_\xd3~ct\xf3b~\xe9r_\xd1~\xf3v\xe9m~b\xe9r_~D\xe9c\xe9~mb\xe9r".split("_"),monthsShort:"J~\xe1\xf1_~F\xe9b_~M\xe1r_~\xc1pr_~M\xe1\xfd_~J\xfa\xf1_~J\xfal_~\xc1\xfag_~S\xe9p_~\xd3ct_~\xd1\xf3v_~D\xe9c".split("_"),monthsParseExact:!0,weekdays:"S~\xfa\xf1d\xe1~\xfd_M\xf3~\xf1d\xe1\xfd~_T\xfa\xe9~sd\xe1\xfd~_W\xe9d~\xf1\xe9sd~\xe1\xfd_T~h\xfars~d\xe1\xfd_~Fr\xedd~\xe1\xfd_S~\xe1t\xfar~d\xe1\xfd".split("_"),weekdaysShort:"S~\xfa\xf1_~M\xf3\xf1_~T\xfa\xe9_~W\xe9d_~Th\xfa_~Fr\xed_~S\xe1t".split("_"),weekdaysMin:"S~\xfa_M\xf3~_T\xfa_~W\xe9_T~h_Fr~_S\xe1".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},calendar:{sameDay:"[T~\xf3d\xe1~\xfd \xe1t] LT",nextDay:"[T~\xf3m\xf3~rr\xf3~w \xe1t] LT",nextWeek:"dddd [\xe1t] LT",lastDay:"[\xdd~\xe9st~\xe9rd\xe1~\xfd \xe1t] LT",lastWeek:"[L~\xe1st] dddd [\xe1t] LT",sameElse:"L"},relativeTime:{future:"\xed~\xf1 %s",past:"%s \xe1~g\xf3",s:"\xe1 ~f\xe9w ~s\xe9c\xf3~\xf1ds",ss:"%d s~\xe9c\xf3\xf1~ds",m:"\xe1 ~m\xed\xf1~\xfat\xe9",mm:"%d m~\xed\xf1\xfa~t\xe9s",h:"\xe1~\xf1 h\xf3~\xfar",hh:"%d h~\xf3\xfars",d:"\xe1 ~d\xe1\xfd",dd:"%d d~\xe1\xfds",M:"\xe1 ~m\xf3\xf1~th",MM:"%d m~\xf3\xf1t~hs",y:"\xe1 ~\xfd\xe9\xe1r",yy:"%d \xfd~\xe9\xe1rs"},dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var a=e%10;return e+(1==~~(e%100/10)?"th":1===a?"st":2===a?"nd":3===a?"rd":"th")},week:{dow:1,doy:4}}),e.defineLocale("yo",{months:"S\u1eb9\u0301r\u1eb9\u0301_E\u0300re\u0300le\u0300_\u1eb8r\u1eb9\u0300na\u0300_I\u0300gbe\u0301_E\u0300bibi_O\u0300ku\u0300du_Ag\u1eb9mo_O\u0300gu\u0301n_Owewe_\u1ecc\u0300wa\u0300ra\u0300_Be\u0301lu\u0301_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),monthsShort:"S\u1eb9\u0301r_E\u0300rl_\u1eb8rn_I\u0300gb_E\u0300bi_O\u0300ku\u0300_Ag\u1eb9_O\u0300gu\u0301_Owe_\u1ecc\u0300wa\u0300_Be\u0301l_\u1ecc\u0300p\u1eb9\u0300\u0300".split("_"),weekdays:"A\u0300i\u0300ku\u0301_Aje\u0301_I\u0300s\u1eb9\u0301gun_\u1eccj\u1ecd\u0301ru\u0301_\u1eccj\u1ecd\u0301b\u1ecd_\u1eb8ti\u0300_A\u0300ba\u0301m\u1eb9\u0301ta".split("_"),weekdaysShort:"A\u0300i\u0300k_Aje\u0301_I\u0300s\u1eb9\u0301_\u1eccjr_\u1eccjb_\u1eb8ti\u0300_A\u0300ba\u0301".split("_"),weekdaysMin:"A\u0300i\u0300_Aj_I\u0300s_\u1eccr_\u1eccb_\u1eb8t_A\u0300b".split("_"),longDateFormat:{LT:"h:mm A",LTS:"h:mm:ss A",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY h:mm A",LLLL:"dddd, D MMMM YYYY h:mm A"},calendar:{sameDay:"[O\u0300ni\u0300 ni] LT",nextDay:"[\u1ecc\u0300la ni] LT",nextWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301n'b\u1ecd] [ni] LT",lastDay:"[A\u0300na ni] LT",lastWeek:"dddd [\u1eccs\u1eb9\u0300 to\u0301l\u1ecd\u0301] [ni] LT",sameElse:"L"},relativeTime:{future:"ni\u0301 %s",past:"%s k\u1ecdja\u0301",s:"i\u0300s\u1eb9ju\u0301 aaya\u0301 die",ss:"aaya\u0301 %d",m:"i\u0300s\u1eb9ju\u0301 kan",mm:"i\u0300s\u1eb9ju\u0301 %d",h:"wa\u0301kati kan",hh:"wa\u0301kati %d",d:"\u1ecdj\u1ecd\u0301 kan",dd:"\u1ecdj\u1ecd\u0301 %d",M:"osu\u0300 kan",MM:"osu\u0300 %d",y:"\u1ecddu\u0301n kan",yy:"\u1ecddu\u0301n %d"},dayOfMonthOrdinalParse:/\u1ecdj\u1ecd\u0301\s\d{1,2}/,ordinal:"\u1ecdj\u1ecd\u0301 %d",week:{dow:1,doy:4}}),e.defineLocale("zh-cn",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u5468\u65e5_\u5468\u4e00_\u5468\u4e8c_\u5468\u4e09_\u5468\u56db_\u5468\u4e94_\u5468\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5Ah\u70b9mm\u5206",LLLL:"YYYY\u5e74M\u6708D\u65e5ddddAh\u70b9mm\u5206",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:e>=11?e:e+12},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u5468)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u5468";default:return e}},relativeTime:{future:"%s\u5185",past:"%s\u524d",s:"\u51e0\u79d2",ss:"%d \u79d2",m:"1 \u5206\u949f",mm:"%d \u5206\u949f",h:"1 \u5c0f\u65f6",hh:"%d \u5c0f\u65f6",d:"1 \u5929",dd:"%d \u5929",M:"1 \u4e2a\u6708",MM:"%d \u4e2a\u6708",y:"1 \u5e74",yy:"%d \u5e74"},week:{dow:1,doy:4}}),e.defineLocale("zh-hk",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e2d\u5348"===a?e>=11?e:e+12:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:void 0},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u9031";default:return e}},relativeTime:{future:"%s\u5167",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}}),e.defineLocale("zh-tw",{months:"\u4e00\u6708_\u4e8c\u6708_\u4e09\u6708_\u56db\u6708_\u4e94\u6708_\u516d\u6708_\u4e03\u6708_\u516b\u6708_\u4e5d\u6708_\u5341\u6708_\u5341\u4e00\u6708_\u5341\u4e8c\u6708".split("_"),monthsShort:"1\u6708_2\u6708_3\u6708_4\u6708_5\u6708_6\u6708_7\u6708_8\u6708_9\u6708_10\u6708_11\u6708_12\u6708".split("_"),weekdays:"\u661f\u671f\u65e5_\u661f\u671f\u4e00_\u661f\u671f\u4e8c_\u661f\u671f\u4e09_\u661f\u671f\u56db_\u661f\u671f\u4e94_\u661f\u671f\u516d".split("_"),weekdaysShort:"\u9031\u65e5_\u9031\u4e00_\u9031\u4e8c_\u9031\u4e09_\u9031\u56db_\u9031\u4e94_\u9031\u516d".split("_"),weekdaysMin:"\u65e5_\u4e00_\u4e8c_\u4e09_\u56db_\u4e94_\u516d".split("_"),longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY\u5e74M\u6708D\u65e5",LLL:"YYYY\u5e74M\u6708D\u65e5 HH:mm",LLLL:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm",l:"YYYY/M/D",ll:"YYYY\u5e74M\u6708D\u65e5",lll:"YYYY\u5e74M\u6708D\u65e5 HH:mm",llll:"YYYY\u5e74M\u6708D\u65e5dddd HH:mm"},meridiemParse:/\u51cc\u6668|\u65e9\u4e0a|\u4e0a\u5348|\u4e2d\u5348|\u4e0b\u5348|\u665a\u4e0a/,meridiemHour:function(e,a){return 12===e&&(e=0),"\u51cc\u6668"===a||"\u65e9\u4e0a"===a||"\u4e0a\u5348"===a?e:"\u4e2d\u5348"===a?e>=11?e:e+12:"\u4e0b\u5348"===a||"\u665a\u4e0a"===a?e+12:void 0},meridiem:function(e,a,t){var s=100*e+a;return s<600?"\u51cc\u6668":s<900?"\u65e9\u4e0a":s<1130?"\u4e0a\u5348":s<1230?"\u4e2d\u5348":s<1800?"\u4e0b\u5348":"\u665a\u4e0a"},calendar:{sameDay:"[\u4eca\u5929]LT",nextDay:"[\u660e\u5929]LT",nextWeek:"[\u4e0b]ddddLT",lastDay:"[\u6628\u5929]LT",lastWeek:"[\u4e0a]ddddLT",sameElse:"L"},dayOfMonthOrdinalParse:/\d{1,2}(\u65e5|\u6708|\u9031)/,ordinal:function(e,a){switch(a){case"d":case"D":case"DDD":return e+"\u65e5";case"M":return e+"\u6708";case"w":case"W":return e+"\u9031";default:return e}},relativeTime:{future:"%s\u5167",past:"%s\u524d",s:"\u5e7e\u79d2",ss:"%d \u79d2",m:"1 \u5206\u9418",mm:"%d \u5206\u9418",h:"1 \u5c0f\u6642",hh:"%d \u5c0f\u6642",d:"1 \u5929",dd:"%d \u5929",M:"1 \u500b\u6708",MM:"%d \u500b\u6708",y:"1 \u5e74",yy:"%d \u5e74"}}),e.locale("en"),e}); \ No newline at end of file diff --git a/domain-server/resources/web/css/bootstrap-sortable.css b/domain-server/resources/web/css/bootstrap-sortable.css new file mode 100755 index 0000000000..aed89cd62e --- /dev/null +++ b/domain-server/resources/web/css/bootstrap-sortable.css @@ -0,0 +1,110 @@ +/** + * adding sorting ability to HTML tables with Bootstrap styling + * @summary HTML tables sorting ability + * @version 2.0.0 + * @requires tinysort, moment.js, jQuery + * @license MIT + * @author Matus Brlit (drvic10k) + * @copyright Matus Brlit (drvic10k), bootstrap-sortable contributors + */ + +table.sortable span.sign { + display: block; + position: absolute; + top: 50%; + right: 5px; + font-size: 12px; + margin-top: -10px; + color: #bfbfc1; +} + +table.sortable th:after { + display: block; + position: absolute; + top: 50%; + right: 5px; + font-size: 12px; + margin-top: -10px; + color: #bfbfc1; +} + +table.sortable th.arrow:after { + content: ''; +} + +table.sortable span.arrow, span.reversed, th.arrow.down:after, th.reversedarrow.down:after, th.arrow.up:after, th.reversedarrow.up:after { + border-style: solid; + border-width: 5px; + font-size: 0; + border-color: #ccc transparent transparent transparent; + line-height: 0; + height: 0; + width: 0; + margin-top: -2px; +} + + table.sortable span.arrow.up, th.arrow.up:after { + border-color: transparent transparent #ccc transparent; + margin-top: -7px; + } + +table.sortable span.reversed, th.reversedarrow.down:after { + border-color: transparent transparent #ccc transparent; + margin-top: -7px; +} + + table.sortable span.reversed.up, th.reversedarrow.up:after { + border-color: #ccc transparent transparent transparent; + margin-top: -2px; + } + +table.sortable span.az:before, th.az.down:after { + content: "a .. z"; +} + +table.sortable span.az.up:before, th.az.up:after { + content: "z .. a"; +} + +table.sortable th.az.nosort:after, th.AZ.nosort:after, th._19.nosort:after, th.month.nosort:after { + content: ".."; +} + +table.sortable span.AZ:before, th.AZ.down:after { + content: "A .. Z"; +} + +table.sortable span.AZ.up:before, th.AZ.up:after { + content: "Z .. A"; +} + +table.sortable span._19:before, th._19.down:after { + content: "1 .. 9"; +} + +table.sortable span._19.up:before, th._19.up:after { + content: "9 .. 1"; +} + +table.sortable span.month:before, th.month.down:after { + content: "jan .. dec"; +} + +table.sortable span.month.up:before, th.month.up:after { + content: "dec .. jan"; +} + +table.sortable>thead th:not([data-defaultsort=disabled]) { + cursor: pointer; + position: relative; + top: 0; + left: 0; +} + +table.sortable>thead th:hover:not([data-defaultsort=disabled]) { + background: #efefef; +} + +table.sortable>thead th div.mozilla { + position: relative; +} diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 5121b85a42..2bcc870ecf 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -355,21 +355,31 @@ table .headers + .headers td { } } -ul.nav li.dropdown ul.dropdown-menu { +ul.dropdown-menu { padding: 0px 0px; } -ul.nav li.dropdown li a { +ul.dropdown-menu li a { padding-top: 7px; padding-bottom: 7px; } -ul.nav li.dropdown li a:hover { +ul.dropdown-menu li a:hover { color: white; background-color: #337ab7; } -ul.nav li.dropdown ul.dropdown-menu .divider { +table ul.dropdown-menu li:first-child a:hover { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +ul.dropdown-menu li:last-child a:hover { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; +} + +ul.dropdown-menu .divider { margin: 0px 0; } @@ -434,3 +444,37 @@ ul.nav li.dropdown ul.dropdown-menu .divider { .save-button-text { pointer-events: none; } + +#content_archives .panel-body { + padding: 0; +} + +#content_archives .panel-body .form-group { + padding: 15px; +} + +#content_archives .panel-body th, #content_archives .panel-body td { + padding: 8px 15px; +} + +#content_archives table { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; +} + +tr.gray-tr { + background-color: #f5f5f5; +} + +.dropdown-toggle span.glyphicon-option-vertical { + font-size: 110%; + cursor: pointer; + border-radius: 50%; + background-color: #F5F5F5; + padding: 4px 4px 4px 6px; +} + +.dropdown.open span.glyphicon-option-vertical { + background-color: #337AB7; + color: white; +} diff --git a/domain-server/resources/web/header.html b/domain-server/resources/web/header.html index 1b7b306fff..bf1d1d1df1 100644 --- a/domain-server/resources/web/header.html +++ b/domain-server/resources/web/header.html @@ -9,6 +9,7 @@ + diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 17f06f3ad1..961a7df3b2 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -106,8 +106,12 @@ function reloadSettings(callback) { $.getJSON(Settings.endpoint, function(data){ _.extend(data, viewHelpers); - for (var spliceIndex in Settings.extraGroups) { - data.descriptions.splice(spliceIndex, 0, Settings.extraGroups[spliceIndex]); + for (var spliceIndex in Settings.extraGroupsAtIndex) { + data.descriptions.splice(spliceIndex, 0, Settings.extraGroupsAtIndex[spliceIndex]); + } + + for (var endGroupIndex in Settings.extraGroupsAtEnd) { + data.descriptions.push(Settings.extraGroupsAtEnd[endGroupIndex]); } $('#panels').html(Settings.panelsTemplate(data)); diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 2f75794786..184b2b954f 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -55,6 +55,34 @@ $(document).ready(function(){ var $contentDropdown = $('#content-settings-nav-dropdown'); var $settingsDropdown = $('#domain-settings-nav-dropdown'); + // define extra groups to add to setting panels, with their splice index + Settings.extraContentGroupsAtIndex = { + 0: { + html_id: Settings.CONTENT_ARCHIVES_PANEL_ID, + label: 'Content Archives' + }, + 1: { + html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID, + label: 'Upload Content' + } + }; + + Settings.extraContentGroupsAtEnd = []; + + Settings.extraDomainGroupsAtIndex = { + 1: { + html_id: 'places', + label: 'Places' + } + } + + Settings.extraDomainGroupsAtEnd = [ + { + html_id: 'settings_backup', + label: 'Settings Backup' + } + ] + // for pages that have the settings dropdowns if ($contentDropdown.length && $settingsDropdown.length) { // make a JSON request to get the dropdown menus for content and settings @@ -65,6 +93,15 @@ $(document).ready(function(){ return "
  • " + group.label + "
  • "; } + // add the dummy settings groups that get populated via JS + for (var spliceIndex in Settings.extraContentGroupsAtIndex) { + data.content_settings.splice(spliceIndex, 0, Settings.extraContentGroupsAtIndex[spliceIndex]); + } + + for (var endIndex in Settings.extraContentGroupsAtEnd) { + data.content_settings.push(Settings.extraContentGroupsAtIndex[spliceIndex]); + } + $.each(data.content_settings, function(index, group){ if (index > 0) { $contentDropdown.append(""); @@ -73,25 +110,22 @@ $(document).ready(function(){ $contentDropdown.append(makeGroupDropdownElement(group, "/content/")); }); + // add the dummy settings groups that get populated via JS + for (var spliceIndex in Settings.extraDomainGroupsAtIndex) { + data.domain_settings.splice(spliceIndex, 0, Settings.extraDomainGroupsAtIndex[spliceIndex]); + } + + for (var endIndex in Settings.extraDomainGroupsAtEnd) { + data.domain_settings.push(Settings.extraDomainGroupsAtEnd[endIndex]); + } + $.each(data.domain_settings, function(index, group){ if (index > 0) { $settingsDropdown.append(""); } $settingsDropdown.append(makeGroupDropdownElement(group, "/settings/")); - - // for domain settings, we add a dummy "Places" group that we fill - // via the API - add it to the dropdown menu in the right spot - // which is after "Metaverse / Networking" - if (group.name == "metaverse") { - $settingsDropdown.append(""); - $settingsDropdown.append(makeGroupDropdownElement({ html_id: 'places', label: 'Places' }, "/settings/")); - } }); - - // append a link for the "Settings Backup" panel - $settingsDropdown.append(""); - $settingsDropdown.append(makeGroupDropdownElement({ html_id: 'settings_backup', label: 'Settings Backup'}, "/settings")); }); } }); diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 69721ee924..040d8959e7 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -42,7 +42,9 @@ Object.assign(Settings, { ADD_PLACE_BTN_ID: 'add-place-btn', FORM_ID: 'settings-form', INVALID_ROW_CLASS: 'invalid-input', - DATA_ROW_INDEX: 'data-row-index' + DATA_ROW_INDEX: 'data-row-index', + CONTENT_ARCHIVES_PANEL_ID: 'content_archives', + UPLOAD_CONTENT_BACKUP_PANEL_ID: 'upload_content' }); var URLs = { @@ -164,7 +166,7 @@ function getDomainFromAPI(callback) { if (callback === undefined) { callback = function() {}; } - + if (!domainIDIsSet()) { callback({ status: 'fail' }); return null; diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 68684c9106..d4de0d5f4c 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -14,17 +14,9 @@ $(document).ready(function(){ return b; })(window.location.search.substr(1).split('&')); - // define extra groups to add to description, with their splice index - Settings.extraGroups = { - 1: { - html_id: 'places', - label: 'Places' - }, - "-1": { - html_id: 'settings_backup', - label: 'Settings Backup' - } - } + Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd; + Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; + Settings.afterReloadActions = function() { // append the domain selection modal @@ -643,7 +635,6 @@ $(document).ready(function(){ autoNetworkingEl.after(form); } - function setupPlacesTable() { // create a dummy table using our view helper var placesTableSetting = { @@ -1097,8 +1088,5 @@ $(document).ready(function(){ html += ""; $('#settings_backup .panel-body').html(html); - - // add an upload button to the footer to kick off the upload form - } }); From dd5a705836d22a9d34d8d56b672d77115e817557 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 13:29:42 -0800 Subject: [PATCH 062/157] move rolling interval backup rules to automatic content archives --- .../resources/describe-settings.json | 130 +++++++++--------- .../resources/web/content/js/content.js | 30 +++- .../resources/web/js/base-settings.js | 4 + .../src/DomainContentBackupManager.cpp | 6 +- domain-server/src/DomainServer.cpp | 9 +- .../src/DomainServerSettingsManager.cpp | 17 +++ .../src/DomainServerSettingsManager.h | 1 + 7 files changed, 123 insertions(+), 74 deletions(-) diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 93d703c8b3..427dc62520 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.1, + "version": 2.2, "settings": [ { "name": "metaverse", @@ -1321,73 +1321,6 @@ "default": "30000", "advanced": true }, - { - "name": "backups", - "type": "table", - "label": "Backup Rules", - "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.", - "numbered": false, - "can_add_new_rows": true, - "default": [ - { - "Name": "Half Hourly Rolling", - "backupInterval": 1800, - "format": ".backup.halfhourly.%N", - "maxBackupVersions": 5 - }, - { - "Name": "Daily Rolling", - "backupInterval": 86400, - "format": ".backup.daily.%N", - "maxBackupVersions": 7 - }, - { - "Name": "Weekly Rolling", - "backupInterval": 604800, - "format": ".backup.weekly.%N", - "maxBackupVersions": 4 - }, - { - "Name": "Thirty Day Rolling", - "backupInterval": 2592000, - "format": ".backup.thirtyday.%N", - "maxBackupVersions": 12 - } - ], - "columns": [ - { - "name": "Name", - "label": "Name", - "can_set": true, - "placeholder": "Example", - "default": "Example" - }, - { - "name": "format", - "label": "Rule Format", - "can_set": true, - "help": "Format used to create the extension for the backup of your persisted entities. Use a format with %N to get rolling. Or use date formatting like %Y-%m-%d.%H:%M:%S.%z", - "placeholder": ".backup.example.%N", - "default": ".backup.example.%N" - }, - { - "name": "backupInterval", - "label": "Backup Interval in Seconds", - "help": "Interval between backup checks in seconds.", - "placeholder": 1800, - "default": 1800, - "can_set": true - }, - { - "name": "maxBackupVersions", - "label": "Max Rolled Backup Versions", - "help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?", - "placeholder": 5, - "default": 5, - "can_set": true - } - ] - }, { "name": "NoPersist", "type": "checkbox", @@ -1649,6 +1582,67 @@ } ] }, + { + "name": "automatic_content_archives", + "label": "Automatic Content Archives", + "settings": [ + { + "name": "backup_rules", + "type": "table", + "label": "Rolling Backup Rules", + "help": "Define how frequently to create automatic content archives", + "numbered": false, + "can_add_new_rows": true, + "default": [ + { + "Name": "Half Hourly Rolling", + "backupInterval": 1800, + "maxBackupVersions": 5 + }, + { + "Name": "Daily Rolling", + "backupInterval": 86400, + "maxBackupVersions": 7 + }, + { + "Name": "Weekly Rolling", + "backupInterval": 604800, + "maxBackupVersions": 4 + }, + { + "Name": "Thirty Day Rolling", + "backupInterval": 2592000, + "maxBackupVersions": 12 + } + ], + "columns": [ + { + "name": "Name", + "label": "Name", + "can_set": true, + "placeholder": "Example", + "default": "Example" + }, + { + "name": "backupInterval", + "label": "Backup Interval in Seconds", + "help": "Interval between backup checks in seconds.", + "placeholder": 1800, + "default": 1800, + "can_set": true + }, + { + "name": "maxBackupVersions", + "label": "Max Rolled Backup Versions", + "help": "If your backup extension format uses 'rolling', how many versions do you want us to keep?", + "placeholder": 5, + "default": 5, + "can_set": true + } + ] + } + ] + }, { "name": "wizard", "label": "Setup Wizard", diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index e2b653995f..0fd5f37a94 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -23,15 +23,17 @@ $(document).ready(function(){ var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody'; + var AUTO_ARCHIVES_SETTINGS_LINK_ID = 'auto-archives-settings-link'; + var automaticBackups = []; var manualBackups = []; function setupContentArchives() { - // construct the HTML needed for the content archives panel var html = "
    "; html += ""; - html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your domain content and settings backups." + html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your domain content and settings backups. " + html += "Click here to manage automatic content archive intervals."; html += "
    "; html += ""; @@ -120,6 +122,30 @@ $(document).ready(function(){ }); } + // handle click on automatic content archive settings link + $('body').on('click', '#' + AUTO_ARCHIVES_SETTINGS_LINK_ID, function(e) { + if (Settings.pendingChanges > 0) { + // don't follow the link right away, make sure the user knows they are about to leave + // the page and lose changes + e.preventDefault(); + + var settingsLink = $(this).attr('href'); + + swal({ + title: "Are you sure?", + text: "You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.", + type: "warning", + showCancelButton: true, + confirmButtonText: "Leave and Lose Pending Changes", + closeOnConfirm: true + }, + function () { + // user wants to drop their changes, switch pages + window.location = settingsLink; + }); + } + }); + // handle click on manual archive creation button $('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) { e.preventDefault(); diff --git a/domain-server/resources/web/js/base-settings.js b/domain-server/resources/web/js/base-settings.js index 961a7df3b2..3476792222 100644 --- a/domain-server/resources/web/js/base-settings.js +++ b/domain-server/resources/web/js/base-settings.js @@ -126,6 +126,8 @@ function reloadSettings(callback) { $('[data-toggle="tooltip"]').tooltip(); + Settings.pendingChanges = 0; + // call the callback now that settings are loaded callback(true); }).fail(function() { @@ -805,6 +807,8 @@ function badgeForDifferences(changedElement) { } }); + Settings.pendingChanges = totalChanges; + if (totalChanges == 0) { totalChanges = "" } diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index a711d2112d..345faffec4 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -63,9 +63,9 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire } void DomainContentBackupManager::parseSettings(const QJsonObject& settings) { - qDebug() << settings << settings["backups"] << settings["backups"].isArray(); - if (settings["backups"].isArray()) { - const QJsonArray& backupRules = settings["backups"].toArray(); + static const QString BACKUP_RULES_KEY = "backup_rules"; + if (settings[BACKUP_RULES_KEY].isArray()) { + const QJsonArray& backupRules = settings[BACKUP_RULES_KEY].toArray(); qCDebug(domain_server) << "BACKUP RULES:"; for (const QJsonValue& value : backupRules) { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 157eaa483f..8247e12de5 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -296,8 +296,15 @@ DomainServer::DomainServer(int argc, char* argv[]) : qCDebug(domain_server) << "Created entities data directory"; } maybeHandleReplacementEntityFile(); + + auto contentArchivesGroup = _settingsManager.valueOrDefaultValueForKeyPath(AUTOMATIC_CONTENT_ARCHIVES_GROUP); + auto archivesIntervalObject = QJsonObject(); - _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), _settingsManager.settingsResponseObjectForType("6")["entity_server_settings"].toObject())); + if (contentArchivesGroup.canConvert()) { + archivesIntervalObject = QJsonObject::fromVariantMap(contentArchivesGroup.toMap()); + } + + _contentManager.reset(new DomainContentBackupManager(getContentBackupDir(), archivesIntervalObject)); connect(_contentManager.get(), &DomainContentBackupManager::started, _contentManager.get(), [this](){ _contentManager->addBackupHandler(BackupHandlerPointer(new EntitiesBackupHandler(getEntitiesFilePath(), getEntitiesReplacementFilePath()))); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 85d6a046b5..a50cde0807 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -393,6 +393,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canRezTemporaryCertifiedEntities); packPermissions(); } + if (oldVersion < 2.0) { const QString WIZARD_COMPLETED_ONCE = "wizard.completed_once"; @@ -400,6 +401,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList *wizardCompletedOnce = QVariant(true); } + if (oldVersion < 2.1) { // convert old avatar scale settings into avatar height. @@ -421,6 +423,21 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } } + if (oldVersion < 2.2) { + // migrate entity server rolling backup intervals to new location for automatic content archive intervals + + const QString ENTITY_SERVER_BACKUPS_KEYPATH = "entity_server_settings.backups"; + const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = "automatic_content_archives.backup_rules"; + + QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH); + + if (previousBackupsVariant) { + auto migratedBackupsVariant = _configMap.valueForKeyPath(AUTO_CONTENT_ARCHIVES_RULES_KEYPATH, true); + *migratedBackupsVariant = *previousBackupsVariant; + } + } + + // write the current description version to our settings *versionVariant = _descriptionVersion; diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index abc70751a8..897a15485f 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -37,6 +37,7 @@ const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions"; const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_fingerprint_permissions"; const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; +const QString AUTOMATIC_CONTENT_ARCHIVES_GROUP = "automatic_content_archives"; using GroupByUUIDKey = QPair; // groupID, rankID From 2d9f2ebf81c0de164948c372e26e8bba286c7bb9 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 13:50:46 -0800 Subject: [PATCH 063/157] fix active with anchor and settings dropdowns on non-settings pages --- domain-server/resources/web/base-settings-scripts.html | 1 - domain-server/resources/web/footer.html | 1 + domain-server/resources/web/js/domain-server.js | 6 +++--- domain-server/resources/web/settings/js/settings.js | 1 - domain-server/resources/web/wizard/index.shtml | 1 - 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/domain-server/resources/web/base-settings-scripts.html b/domain-server/resources/web/base-settings-scripts.html index fe370c4675..877b0a6125 100644 --- a/domain-server/resources/web/base-settings-scripts.html +++ b/domain-server/resources/web/base-settings-scripts.html @@ -3,5 +3,4 @@ - diff --git a/domain-server/resources/web/footer.html b/domain-server/resources/web/footer.html index e8ea392b49..49e883509e 100644 --- a/domain-server/resources/web/footer.html +++ b/domain-server/resources/web/footer.html @@ -1,4 +1,5 @@ + diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 184b2b954f..6b2d4e1316 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -28,7 +28,7 @@ function settingsGroupAnchor(base, html_id) { } $(document).ready(function(){ - var url = window.location; + var url = location.protocol + '//' + location.host+location.pathname; // Will only work if string in href matches with location $('ul.nav a[href="'+ url +'"]').parent().addClass('active'); @@ -55,7 +55,7 @@ $(document).ready(function(){ var $contentDropdown = $('#content-settings-nav-dropdown'); var $settingsDropdown = $('#domain-settings-nav-dropdown'); - // define extra groups to add to setting panels, with their splice index + // define extra groups to add to setting panels, with their splice index Settings.extraContentGroupsAtIndex = { 0: { html_id: Settings.CONTENT_ARCHIVES_PANEL_ID, @@ -99,7 +99,7 @@ $(document).ready(function(){ } for (var endIndex in Settings.extraContentGroupsAtEnd) { - data.content_settings.push(Settings.extraContentGroupsAtIndex[spliceIndex]); + data.content_settings.push(Settings.extraContentGroupsAtEnd[endIndex]); } $.each(data.content_settings, function(index, group){ diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index d4de0d5f4c..1c6510298f 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -17,7 +17,6 @@ $(document).ready(function(){ Settings.extraGroupsAtEnd = Settings.extraDomainGroupsAtEnd; Settings.extraGroupsAtIndex = Settings.extraDomainGroupsAtIndex; - Settings.afterReloadActions = function() { // append the domain selection modal appendDomainIDButtons(); diff --git a/domain-server/resources/web/wizard/index.shtml b/domain-server/resources/web/wizard/index.shtml index b526a5719b..5a3286296d 100644 --- a/domain-server/resources/web/wizard/index.shtml +++ b/domain-server/resources/web/wizard/index.shtml @@ -261,6 +261,5 @@ - From b019895fce5051e18188d75a89c1a4ce791d3765 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 15:49:42 -0800 Subject: [PATCH 064/157] handle entity file upload from new content upload --- .../resources/web/content/js/content.js | 58 +++++++++++++++++-- .../resources/web/js/domain-server.js | 2 +- .../resources/web/settings/js/settings.js | 53 ++++++++++------- domain-server/src/DomainServer.cpp | 30 ++++++++-- 4 files changed, 111 insertions(+), 32 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 0fd5f37a94..4e2c27bf54 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -7,17 +7,67 @@ $(document).ready(function(){ // construct the HTML needed for the settings backup panel var html = "
    "; - html += "Upload a Content Backup to replace the content of this domain"; - html += "
    Note: Your domain's content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
    "; + html += "Upload a Content Archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; + html += "
    Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
    "; html += ""; - html += ""; + html += ""; html += "
    "; $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); } + // handle content archive or entity file upload + + // when the selected file is changed, enable the button if there's a selected file + $('body').on('change', '#' + RESTORE_SETTINGS_FILE_ID, function() { + if ($(this).val()) { + $('#' + RESTORE_SETTINGS_UPLOAD_ID).attr('disabled', false); + } + }); + + // when the upload button is clicked, send the file to the DS + // and reload the page if restore was successful or + // show an error if not + $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ + e.preventDefault(); + + swal({ + title: "Are you sure?", + text: "Your domain content will be replaced by the uploaded Content Archive or entity file", + type: "warning", + showCancelButton: true, + closeOnConfirm: false + }, + function () { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); + + showSpinnerAlert("Restoring Content"); + + $.ajax({ + url: '/content/upload', + type: 'POST', + cache: false, + processData: false, + contentType: false, + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "Please ensure that the content archive or entity file is valid and try again." + ); + }); + }); + }); + var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; @@ -136,7 +186,7 @@ $(document).ready(function(){ text: "You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.", type: "warning", showCancelButton: true, - confirmButtonText: "Leave and Lose Pending Changes", + confirmButtonText: "Proceed without Saving", closeOnConfirm: true }, function () { diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 6b2d4e1316..d3b20d40bb 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -79,7 +79,7 @@ $(document).ready(function(){ Settings.extraDomainGroupsAtEnd = [ { html_id: 'settings_backup', - label: 'Settings Backup' + label: 'Settings Backup / Restore' } ] diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 1c6510298f..b73337ef2d 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -1033,31 +1033,40 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + swal({ + title: "Are you sure?", + text: "Your domain settings will be replaced by the uploaded settings", + type: "warning", + showCancelButton: true, + closeOnConfirm: false + }, + function() { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); - var fileFormData = new FormData(); - fileFormData.append('restore-file', files[0]); + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Restoring Settings"); + showSpinnerAlert("Restoring Settings"); - $.ajax({ - url: '/settings/restore', - type: 'POST', - processData: false, - contentType: false, - dataType: 'json', - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - swal.close(); - showRestartModal(); - }).fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain settings.\n" - + "Please ensure that your current domain settings are valid and try again." - ); + $.ajax({ + url: '/settings/restore', + type: 'POST', + processData: false, + contentType: false, + dataType: 'json', + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain settings.\n" + + "Please ensure that your current domain settings are valid and try again." + ); - reloadSettings(); + reloadSettings(); + }); }); }); @@ -1079,7 +1088,7 @@ $(document).ready(function(){ html += "
    "; html += ""; html += "Upload a settings configuration to quickly configure this domain"; - html += "
    Note: Your domain's settings will be replaced by the settings you upload
    "; + html += "
    Note: Your domain settings will be replaced by the settings you upload"; html += ""; html += ""; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8247e12de5..f3765f6868 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2256,12 +2256,32 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QList formData = connection->parseFormData(); if (formData.size() > 0 && formData[0].second.size() > 0) { - // invoke our method to hand the new octree file off to the octree server - QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + auto& firstFormData = formData[0]; + + // check the file extension to see what kind of file this is + // to match sure we handle this filetype for a content restore + auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); + auto formDataFilenameRegex = QRegExp("filename=\"(\\S+)\""); + auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue); + + QString uploadedFilename = ""; + if (matchIndex != -1) { + uploadedFilename = formDataFilenameRegex.cap(1); + } + + if (uploadedFilename.endsWith(".json", Qt::CaseInsensitive) + || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { + // invoke our method to hand the new octree file off to the octree server + QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", + Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + + // respond with a 200 for success + connection->respond(HTTPConnection::StatusCode200); + } else { + // we don't have handling for this filetype, send back a 400 for failure + connection->respond(HTTPConnection::StatusCode400); + } - // respond with a 200 for success - connection->respond(HTTPConnection::StatusCode200); } else { // respond with a 400 for failure connection->respond(HTTPConnection::StatusCode400); From 2b39419795166233273eb6688621f66266ddd10d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Wed, 14 Feb 2018 17:42:49 -0800 Subject: [PATCH 065/157] keeping AYS DRY and hooking up restore/delete for content archives --- .../resources/web/content/js/content.js | 165 +++++++++++++----- .../resources/web/js/domain-server.js | 19 +- domain-server/resources/web/js/shared.js | 11 ++ .../resources/web/settings/js/settings.js | 78 ++++----- 4 files changed, 175 insertions(+), 98 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 4e2c27bf54..34af9262a2 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -7,7 +7,7 @@ $(document).ready(function(){ // construct the HTML needed for the settings backup panel var html = "
    "; - html += "Upload a Content Archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; + html += "Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; html += "
    Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
    "; html += ""; @@ -33,39 +33,36 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - swal({ - title: "Are you sure?", - text: "Your domain content will be replaced by the uploaded Content Archive or entity file", - type: "warning", - showCancelButton: true, - closeOnConfirm: false - }, - function () { - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + swalAreYouSure( + "Your domain content will be replaced by the uploaded Content Archive or entity file", + "Restore content", + function() { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); - var fileFormData = new FormData(); - fileFormData.append('restore-file', files[0]); + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Restoring Content"); + showSpinnerAlert("Restoring Content"); - $.ajax({ - url: '/content/upload', - type: 'POST', - cache: false, - processData: false, - contentType: false, - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - swal.close(); - showRestartModal(); - }).fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain content.\n" - + "Please ensure that the content archive or entity file is valid and try again." - ); - }); - }); + $.ajax({ + url: '/content/upload', + type: 'POST', + cache: false, + processData: false, + contentType: false, + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "Please ensure that the content archive or entity file is valid and try again." + ); + }); + } + ); }); var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; @@ -104,6 +101,10 @@ $(document).ready(function(){ $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); } + var BACKUP_RESTORE_LINK_CLASS = 'restore-backup'; + var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; + var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; + function reloadLatestBackups() { // make a GET request to get backup information to populate the table $.get('/api/backups', function(data) { @@ -117,12 +118,15 @@ $(document).ready(function(){ // populate the backups tables with the backups function createBackupTableRow(backup) { - return "
    " + + ""; + + ""; } var automaticRows = ""; @@ -172,6 +176,79 @@ $(document).ready(function(){ }); } + // handle click in table to restore a given content backup + $('body').on('click', '.' + BACKUP_RESTORE_LINK_CLASS, function(e){ + // stop the default behaviour + e.preventDefault(); + + // grab the name of this backup so we can show it in alerts + var backupName = $(this).closest('tr').attr('data-backup-name'); + + // grab the ID of this backup in case we need to send a POST + var backupID = $(this).closest('tr').attr('data-backup-id'); + + // make sure the user knows what is about to happen + swalAreYouSure( + "Your domain content will be replaced by the content archive " + backupName, + "Restore content", + function() { + // show a spinner while we send off our request + showSpinnerAlert("Restoring Content Archive " + backupName); + + // setup an AJAX POST to request content restore + $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain content.\n" + + "If the problem persists, the content archive may be corrupted." + ); + }); + } + ) + }); + + // handle click in table to delete a given content backup + $('body').on('click', '.' + BACKUP_DELETE_LINK_CLASS, function(e){ + // stop the default behaviour + e.preventDefault(); + + // grab the name of this backup so we can show it in alerts + var backupName = $(this).closest('tr').attr('data-backup-name'); + + // grab the ID of this backup in case we need to send the DELETE request + var backupID = $(this).closest('tr').attr('data-backup-id'); + + // make sure the user knows what is about to happen + swalAreYouSure( + "The content archive " + backupName + " will be deleted and will no longer be available for restore or download from this page.", + "Delete content archive", + function() { + // show a spinner while we send off our request + showSpinnerAlert("Deleting content archive " + backupName); + + // setup an AJAX DELETE to request content archive delete + $.ajax({ + url: '/api/backups/' + backupID, + type: 'DELETE' + }).done(function(data, textStatus, jqXHR) { + swal.close(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was an unexpected error deleting the content archive" + ); + }).always(function(){ + // reload the list of content archives in case we deleted a backup + // or it's no longer an available backup for some other reason + reloadContentArchives(); + }); + } + ) + }); + // handle click on automatic content archive settings link $('body').on('click', '#' + AUTO_ARCHIVES_SETTINGS_LINK_ID, function(e) { if (Settings.pendingChanges > 0) { @@ -181,18 +258,14 @@ $(document).ready(function(){ var settingsLink = $(this).attr('href'); - swal({ - title: "Are you sure?", - text: "You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.", - type: "warning", - showCancelButton: true, - confirmButtonText: "Proceed without Saving", - closeOnConfirm: true - }, - function () { - // user wants to drop their changes, switch pages - window.location = settingsLink; - }); + swalAreYouSure( + "You have pending changes to content settings that have not been saved. They will be lost if you leave the page to manage automatic content archive intervals.", + "Proceed without Saving", + function() { + // user wants to drop their changes, switch pages + window.location = settingsLink; + } + ); } }); diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index d3b20d40bb..2c12e2683a 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -39,16 +39,15 @@ $(document).ready(function(){ }).parent().addClass('active'); $('body').on('click', '#restart-server', function(e) { - swal( { - title: "Are you sure?", - text: "This will restart your domain server, causing your domain to be briefly offline.", - type: "warning", - html: true, - showCancelButton: true - }, function() { - $.get("/restart"); - showRestartModal(); - }); + swalAreYouSure( + "This will restart your domain server, causing your domain to be briefly offline.", + "Restart", + function() { + swal.close(); + $.get("/restart"); + showRestartModal(); + } + ) return false; }); diff --git a/domain-server/resources/web/js/shared.js b/domain-server/resources/web/js/shared.js index 040d8959e7..84bba4de56 100644 --- a/domain-server/resources/web/js/shared.js +++ b/domain-server/resources/web/js/shared.js @@ -98,6 +98,17 @@ var DOMAIN_ID_TYPE_TEMP = 1; var DOMAIN_ID_TYPE_FULL = 2; var DOMAIN_ID_TYPE_UNKNOWN = 3; +function swalAreYouSure(text, confirmButtonText, callback) { + swal({ + title: "Are you sure?", + text: text, + type: "warning", + showCancelButton: true, + confirmButtonText: confirmButtonText, + closeOnConfirm: false + }, callback); +} + function domainIDIsSet() { if (typeof Settings.data.values.metaverse !== 'undefined' && typeof Settings.data.values.metaverse.id !== 'undefined') { diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index b73337ef2d..e67ea43158 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -94,20 +94,17 @@ $(document).ready(function(){ var password = formJSON["security"]["http_password"]; if ((password == sha256_digest("")) && (username == undefined || (username && username.length != 0))) { - swal({ - title: "Are you sure?", - text: "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", - type: "warning", - showCancelButton: true, - confirmButtonColor: "#5cb85c", - confirmButtonText: "Yes!", - closeOnConfirm: true - }, - function () { + swalAreYouSure( + "You have entered a blank password with a non-blank username. Are you sure you want to require a blank password?", + "Use blank password", + function() { + swal.close(); + formJSON["security"]["http_password"] = ""; postSettings(formJSON); - }); + } + ); return; } @@ -1033,41 +1030,38 @@ $(document).ready(function(){ $('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){ e.preventDefault(); - swal({ - title: "Are you sure?", - text: "Your domain settings will be replaced by the uploaded settings", - type: "warning", - showCancelButton: true, - closeOnConfirm: false - }, - function() { - var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); + swalAreYouSure( + "Your domain settings will be replaced by the uploaded settings", + "Restore settings", + function() { + var files = $('#' + RESTORE_SETTINGS_FILE_ID).prop('files'); - var fileFormData = new FormData(); - fileFormData.append('restore-file', files[0]); + var fileFormData = new FormData(); + fileFormData.append('restore-file', files[0]); - showSpinnerAlert("Restoring Settings"); + showSpinnerAlert("Restoring Settings"); - $.ajax({ - url: '/settings/restore', - type: 'POST', - processData: false, - contentType: false, - dataType: 'json', - data: fileFormData - }).done(function(data, textStatus, jqXHR) { - swal.close(); - showRestartModal(); - }).fail(function(jqXHR, textStatus, errorThrown) { - showErrorMessage( - "Error", - "There was a problem restoring domain settings.\n" - + "Please ensure that your current domain settings are valid and try again." - ); + $.ajax({ + url: '/settings/restore', + type: 'POST', + processData: false, + contentType: false, + dataType: 'json', + data: fileFormData + }).done(function(data, textStatus, jqXHR) { + swal.close(); + showRestartModal(); + }).fail(function(jqXHR, textStatus, errorThrown) { + showErrorMessage( + "Error", + "There was a problem restoring domain settings.\n" + + "Please ensure that your current domain settings are valid and try again." + ); - reloadSettings(); - }); - }); + reloadSettings(); + }); + } + ); }); $('body').on('change', '#' + RESTORE_SETTINGS_FILE_ID, function() { From 41b0bb8c581ae249b2e3dbb4a915f8912e270fed Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:06:36 -0800 Subject: [PATCH 066/157] connect download link from content archive tables --- domain-server/resources/web/content/js/content.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 34af9262a2..12a6d4b734 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -126,7 +126,7 @@ $(document).ready(function(){ + ""; + + "
  • Delete
  • "; } var automaticRows = ""; From 910f3425f8bf3fa080b68dae1e0b5786e6564517 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:57:07 -0800 Subject: [PATCH 067/157] fix latest backup refreshing with no caching --- domain-server/resources/web/content/js/content.js | 14 +++++++++----- domain-server/src/DomainServer.cpp | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 12a6d4b734..1e5b6ac131 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -107,7 +107,11 @@ $(document).ready(function(){ function reloadLatestBackups() { // make a GET request to get backup information to populate the table - $.get('/api/backups', function(data) { + $.ajax({ + url: '/api/backups', + cache: false + }).done(function(data) { + // split the returned data into manual and automatic manual backups var splitBackups = _.partition(data.backups, function(value, index) { return value.isManualBackup; @@ -126,7 +130,7 @@ $(document).ready(function(){ + ""; + + "
  • Delete
  • "; } var automaticRows = ""; @@ -243,7 +247,7 @@ $(document).ready(function(){ }).always(function(){ // reload the list of content archives in case we deleted a backup // or it's no longer an available backup for some other reason - reloadContentArchives(); + reloadLatestBackups(); }); } ) @@ -295,14 +299,14 @@ $(document).ready(function(){ // post the provided archive name to ask the server to kick off a manual backup $.ajax({ type: 'POST', - url: '/api/backup', + url: '/api/backups', data: { 'name': inputValue } }).done(function(data) { // since we successfully setup a new content archive, reload the table of archives // which should show that this archive is pending creation - reloadContentArchives(); + reloadLatestBackups(); }).fail(function(jqXHR, textStatus, errorThrown) { }); diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index f3765f6868..c23d17ed95 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1941,7 +1941,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url const QString URI_ASSIGNMENT = "/assignment"; const QString URI_NODES = "/nodes"; const QString URI_SETTINGS = "/settings"; - const QString URI_ENTITY_FILE_UPLOAD = "/content/upload"; + const QString URI_CONTENT_UPLOAD = "/content/upload"; const QString URI_RESTART = "/restart"; const QString URI_API_PLACES = "/api/places"; const QString URI_API_DOMAINS = "/api/domains"; @@ -2251,7 +2251,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url connection->respond(HTTPConnection::StatusCode200); return true; - } else if (url.path() == URI_ENTITY_FILE_UPLOAD) { + } else if (url.path() == URI_CONTENT_UPLOAD) { // this is an entity file upload, ask the HTTPConnection to parse the data QList formData = connection->parseFormData(); From 6f8381d3787038175b3dff25d345675efeed9657 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 10:59:48 -0800 Subject: [PATCH 068/157] use automatic content archives group const --- domain-server/src/DomainServerSettingsManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a50cde0807..a3f99facea 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -427,7 +427,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // migrate entity server rolling backup intervals to new location for automatic content archive intervals const QString ENTITY_SERVER_BACKUPS_KEYPATH = "entity_server_settings.backups"; - const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = "automatic_content_archives.backup_rules"; + const QString AUTO_CONTENT_ARCHIVES_RULES_KEYPATH = AUTOMATIC_CONTENT_ARCHIVES_GROUP + ".backup_rules"; QVariant* previousBackupsVariant = _configMap.valueForKeyPath(ENTITY_SERVER_BACKUPS_KEYPATH); From 2020ce5907e05338ec8f7e9fbda3a457dd198df0 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 11:20:02 -0800 Subject: [PATCH 069/157] add API to recover from content archive --- .../src/DomainContentBackupManager.cpp | 54 ++++++++++++++----- .../src/DomainContentBackupManager.h | 3 ++ domain-server/src/DomainServer.cpp | 17 +++++- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 345faffec4..379aa640f8 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -256,6 +257,22 @@ void DomainContentBackupManager::deleteBackup(MiniPromise::Promise promise, cons }); } +bool DomainContentBackupManager::recoverFromBackupZip(QuaZip& zip, const QString& backupName) { + if (!zip.open(QuaZip::Mode::mdUnzip)) { + qWarning() << "Failed to unzip file: " << zip.getZipName(); + return false; + } else { + _isRecovering = true; + + for (auto& handler : _backupHandlers) { + handler->recoverBackup(zip); + } + + qDebug() << "Successfully started recovering from " << zip.getZipName(); + return true; + } +} + void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, const QString& backupName) { if (_isRecovering) { promise->resolve({ @@ -277,19 +294,9 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, QFile backupFile { backupDir.filePath(backupName) }; if (backupFile.open(QIODevice::ReadOnly)) { QuaZip zip { &backupFile }; - if (!zip.open(QuaZip::Mode::mdUnzip)) { - qWarning() << "Failed to unzip file: " << backupName; - success = false; - } else { - _isRecovering = true; - _recoveryFilename = backupName; - for (auto& handler : _backupHandlers) { - handler->recoverBackup(zip); - } - - qDebug() << "Successfully started recovering from " << backupName; - success = true; - } + + success = recoverFromBackupZip(zip, backupName); + backupFile.close(); } else { success = false; @@ -301,7 +308,28 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, }); } +void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise), + Q_ARG(QByteArray, uploadedBackup)); + return; + } + + qDebug() << "Recovering from uploaded content archive"; + + // create a buffer and then a QuaZip from that buffer + QBuffer uploadedBackupBuffer { &uploadedBackup }; + QuaZip uploadedZip { &uploadedBackupBuffer }; + + bool success = recoverFromBackupZip(uploadedZip, MANUAL_BACKUP_PREFIX + "uploaded.zip"); + + promise->resolve({ + { "success", success } + }); +} + std::vector DomainContentBackupManager::getAllBackups() { + QDir backupDir { _backupDirectory }; auto matchingFiles = backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" }, diff --git a/domain-server/src/DomainContentBackupManager.h b/domain-server/src/DomainContentBackupManager.h index f1aa4acab2..d4b1f60b87 100644 --- a/domain-server/src/DomainContentBackupManager.h +++ b/domain-server/src/DomainContentBackupManager.h @@ -63,6 +63,7 @@ public slots: void getAllBackupsAndStatus(MiniPromise::Promise promise); void createManualBackup(MiniPromise::Promise promise, const QString& name); void recoverFromBackup(MiniPromise::Promise promise, const QString& backupName); + void recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup); void deleteBackup(MiniPromise::Promise promise, const QString& backupName); void consolidateBackup(MiniPromise::Promise promise, QString fileName); @@ -85,6 +86,8 @@ protected: std::pair createBackup(const QString& prefix, const QString& name); + bool recoverFromBackupZip(QuaZip& backupZip, const QString& backupName); + private: const QString _backupDirectory; std::vector _backupHandlers; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index c23d17ed95..7fe1d1a0ab 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2273,10 +2273,25 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url || uploadedFilename.endsWith(".json.gz", Qt::CaseInsensitive)) { // invoke our method to hand the new octree file off to the octree server QMetaObject::invokeMethod(this, "handleOctreeFileReplacement", - Qt::QueuedConnection, Q_ARG(QByteArray, formData[0].second)); + Qt::QueuedConnection, Q_ARG(QByteArray, firstFormData.second)); // respond with a 200 for success connection->respond(HTTPConnection::StatusCode200); + } else if (uploadedFilename.endsWith(".zip", Qt::CaseInsensitive)) { + auto deferred = makePromise("recoverFromUploadedBackup"); + + deferred->then([connection, JSON_MIME_TYPE](QString error, QVariantMap result) { + QJsonObject rootJSON; + auto success = result["success"].toBool(); + rootJSON["success"] = success; + QJsonDocument docJSON(rootJSON); + connection->respond(success ? HTTPConnection::StatusCode200 : HTTPConnection::StatusCode400, docJSON.toJson(), + JSON_MIME_TYPE.toUtf8()); + }); + + _contentManager->recoverFromUploadedBackup(deferred, firstFormData.second); + + return true; } else { // we don't have handling for this filetype, send back a 400 for failure connection->respond(HTTPConnection::StatusCode400); From de75fe8e9f04618b0e3e9febbc6c2c6481d37e18 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 15:34:03 -0800 Subject: [PATCH 070/157] CR fix for typo in comment --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7fe1d1a0ab..7cd6cd34fe 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -2259,7 +2259,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto& firstFormData = formData[0]; // check the file extension to see what kind of file this is - // to match sure we handle this filetype for a content restore + // to make sure we handle this filetype for a content restore auto dispositionValue = QString(firstFormData.first.value("Content-Disposition")); auto formDataFilenameRegex = QRegExp("filename=\"(\\S+)\""); auto matchIndex = formDataFilenameRegex.indexIn(dispositionValue); From 40078450dd855cb4dc8ea371ee9bcf5a0d0cab90 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 11:20:02 -0800 Subject: [PATCH 071/157] add API to recover from content archive --- domain-server/src/DomainContentBackupManager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 379aa640f8..40a2a55486 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -309,6 +309,7 @@ void DomainContentBackupManager::recoverFromBackup(MiniPromise::Promise promise, } void DomainContentBackupManager::recoverFromUploadedBackup(MiniPromise::Promise promise, QByteArray uploadedBackup) { + if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "recoverFromUploadedBackup", Q_ARG(MiniPromise::Promise, promise), Q_ARG(QByteArray, uploadedBackup)); From cb747c9cdfc394d53b12ba91a6041cd96f807d10 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Thu, 15 Feb 2018 17:32:53 -0800 Subject: [PATCH 072/157] refresh backups for availability and restore status --- .../resources/web/content/js/content.js | 89 +++++++++++++++---- domain-server/resources/web/css/style.css | 5 ++ .../resources/web/js/domain-server.js | 2 +- 3 files changed, 78 insertions(+), 18 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 1e5b6ac131..69a8c93f82 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -2,10 +2,19 @@ $(document).ready(function(){ var RESTORE_SETTINGS_UPLOAD_ID = 'restore-settings-button'; var RESTORE_SETTINGS_FILE_ID = 'restore-settings-file'; + var UPLOAD_CONTENT_ALLOWED_DIV_ID = 'upload-content-allowed'; + var UPLOAD_CONTENT_RECOVERING_DIV_ID = 'upload-content-recovering'; + + function progressBarHTML(extraClass, label) { + var html = "
    "; + html += "
    "; + html += label + "
    "; + return html; + } function setupBackupUpload() { // construct the HTML needed for the settings backup panel - var html = "
    "; + var html = "
    "; html += "Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain."; html += "
    Note: Your domain content will be replaced by the content you upload, but the existing backup files of your domain's content will not immediately be changed.
    "; @@ -13,7 +22,10 @@ $(document).ready(function(){ html += ""; html += ""; - html += "
    "; + html += "
    "; + html += "Restore in progress"; + html += progressBarHTML('recovery', 'Restoring'); + html += "
    "; $('#' + Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID + ' .panel-body').html(html); } @@ -71,6 +83,7 @@ $(document).ready(function(){ var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; var MANUAL_ARCHIVES_TBODY_ID = 'manual-archives-tbody'; var AUTO_ARCHIVES_SETTINGS_LINK_ID = 'auto-archives-settings-link'; + var ACTION_MENU_CLASS = 'action-menu'; var automaticBackups = []; var manualBackups = []; @@ -84,7 +97,9 @@ $(document).ready(function(){ html += ""; html += "
    " + backup.name + "" + return "
    " + backup.name + "" + moment(backup.createdAtMillis).format('lll') + "" + "" - + "
    "; - var backups_table_head = ""; + var backups_table_head = "" + + "" + + ""; html += backups_table_head; html += "
    Archive NameArchive DateActions
    Archive NameArchive DateActions
    "; @@ -105,7 +120,7 @@ $(document).ready(function(){ var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; - function reloadLatestBackups() { + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ url: '/api/backups', @@ -125,7 +140,7 @@ $(document).ready(function(){ return "" + "" + backup.name + "" + moment(backup.createdAtMillis).format('lll') - + "" + + "" + ""; } + function updateProgressBars($progressBar, value) { + $progressBar.attr('aria-valuenow', value).attr('style', 'width: ' + value + '%'); + $progressBar.find('.sr-only').html(data.status.recoveryProgress + "% Complete"); + } + + function updateOrAddTableRow(backup, tableBodyID) { + // check for a backup with this ID + var $backupRow = $("tr[data-backup-id='" + backup.id + "']"); + + if ($backupRow.length == 0) { + // create a new row and then add it to the table + $backupRow = $(createBackupTableRow(backup)); + $('#' + tableBodyID).append($backupRow); + } + + // update the row status column depending on if it is available or recovering + if (!backup.isAvailable) { + // add a progress bar to the status row for availability + $backupRow.find('td.backup-status').html(progressBarHTML('availability', 'Archiving')); + + // set the value of the progress bar based on availability progress + updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress); + } else if (backup.id == data.status.recoveringBackupId) { + // add a progress bar to the status row for recovery + $backupRow.find('td.backup-status').html(progressBarHTML('recovery', 'Restoring')); + } else { + // no special status for this row, use an empty status column + $backupRow.find('td.backup-status').html(''); + } + + $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + } + var automaticRows = ""; if (automaticBackups.length > 0) { for (var backupIndex in automaticBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - automaticRows += createBackupTableRow(automaticBackups[backupIndex]); + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID) } } - $('#' + AUTOMATIC_ARCHIVES_TBODY_ID).html(automaticRows); - - var manualRows = ""; - if (manualBackups.length > 0) { for (var backupIndex in manualBackups) { - // create a table row for this backup and add it to the rows we'll put in the table body - manualRows += createBackupTableRow(manualBackups[backupIndex]); + updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID) } } - $('#' + MANUAL_ARCHIVES_TBODY_ID).html(manualRows); + // check if the restore action on all rows should be enabled or disabled + $('.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering); + + // hide or show the manual content upload file and button depending on our recovering status + $('#' + UPLOAD_CONTENT_ALLOWED_DIV_ID).toggle(!data.status.isRecovering); + $('#' + UPLOAD_CONTENT_RECOVERING_DIV_ID).toggle(data.status.isRecovering); + + // update the progress bars for current restore status + if (data.status.isRecovering) { + updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress); + } // tell bootstrap sortable to update for the new rows $.bootstrapSortable({ applyLast: true }); @@ -247,7 +299,7 @@ $(document).ready(function(){ }).always(function(){ // reload the list of content archives in case we deleted a backup // or it's no longer an available backup for some other reason - reloadLatestBackups(); + reloadBackupInformation(); }); } ) @@ -306,7 +358,7 @@ $(document).ready(function(){ }).done(function(data) { // since we successfully setup a new content archive, reload the table of archives // which should show that this archive is pending creation - reloadLatestBackups(); + reloadBackupInformation(); }).fail(function(jqXHR, textStatus, errorThrown) { }); @@ -322,6 +374,9 @@ $(document).ready(function(){ setupContentArchives(); // load the latest backups immediately - reloadLatestBackups(); + reloadBackupInformation(); + + // setup a timer to reload them every 5 seconds + setTimeout(reloadBackupInformation(), 5000); }; }); diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 2bcc870ecf..62f442584e 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -466,6 +466,11 @@ tr.gray-tr { background-color: #f5f5f5; } +table .action-menu { + text-align: right; + width: 90px; +} + .dropdown-toggle span.glyphicon-option-vertical { font-size: 110%; cursor: pointer; diff --git a/domain-server/resources/web/js/domain-server.js b/domain-server/resources/web/js/domain-server.js index 2c12e2683a..ed9559b6e9 100644 --- a/domain-server/resources/web/js/domain-server.js +++ b/domain-server/resources/web/js/domain-server.js @@ -62,7 +62,7 @@ $(document).ready(function(){ }, 1: { html_id: Settings.UPLOAD_CONTENT_BACKUP_PANEL_ID, - label: 'Upload Content' + label: 'Upload Content Archive' } }; From faacd986b3924ccc092801a58e79b63f8fe9563f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:37:15 -0800 Subject: [PATCH 073/157] remove deleted backups from content archives tables --- .../resources/web/content/js/content.js | 46 +++++++++++++------ .../src/DomainServerSettingsManager.cpp | 3 +- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 69a8c93f82..5c2e134102 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -78,6 +78,8 @@ $(document).ready(function(){ }); var GENERATE_ARCHIVE_BUTTON_ID = 'generate-archive-button'; + var CONTENT_ARCHIVES_NORMAL_ID = 'content-archives-success'; + var CONTENT_ARCHIVES_ERROR_ID = 'content-archives-error'; var AUTOMATIC_ARCHIVES_TABLE_ID = 'automatic-archives-table'; var AUTOMATIC_ARCHIVES_TBODY_ID = 'automatic-archives-tbody'; var MANUAL_ARCHIVES_TABLE_ID = 'manual-archives-table'; @@ -90,10 +92,10 @@ $(document).ready(function(){ function setupContentArchives() { // construct the HTML needed for the content archives panel - var html = "
    "; + var html = "
    "; html += ""; html += "Your domain server makes regular archives of the content in your domain. In the list below, you can see and download all of your domain content and settings backups. " - html += "Click here to manage automatic content archive intervals."; + html += "Click here to manage automatic content archive intervals."; html += "
    "; html += ""; @@ -110,7 +112,11 @@ $(document).ready(function(){ html += ""; html += "
    "; html += backups_table_head; - html += "
    "; + html += "
    "; + + html += ""; // put the base HTML in the content archives panel $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html(html); @@ -119,7 +125,8 @@ $(document).ready(function(){ var BACKUP_RESTORE_LINK_CLASS = 'restore-backup'; var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; - + var ACTIVE_BACKUP_ROW_CLASS = 'active-backup'; + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ @@ -153,6 +160,10 @@ $(document).ready(function(){ $progressBar.find('.sr-only').html(data.status.recoveryProgress + "% Complete"); } + // before we add any new rows and update existing ones + // remove our flag for active rows + $('.' + ACTIVE_BACKUP_ROW_CLASS).removeClass(ACTIVE_BACKUP_ROW_CLASS); + function updateOrAddTableRow(backup, tableBodyID) { // check for a backup with this ID var $backupRow = $("tr[data-backup-id='" + backup.id + "']"); @@ -169,7 +180,7 @@ $(document).ready(function(){ $backupRow.find('td.backup-status').html(progressBarHTML('availability', 'Archiving')); // set the value of the progress bar based on availability progress - updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress); + updateProgressBars($backupRow.find('.progress-bar'), backup.availabilityProgress * 100); } else if (backup.id == data.status.recoveringBackupId) { // add a progress bar to the status row for recovery $backupRow.find('td.backup-status').html(progressBarHTML('recovery', 'Restoring')); @@ -179,22 +190,29 @@ $(document).ready(function(){ } $backupRow.find('td.' + ACTION_MENU_CLASS + ' .dropdown').toggle(backup.isAvailable); + + $backupRow.addClass(ACTIVE_BACKUP_ROW_CLASS); } var automaticRows = ""; if (automaticBackups.length > 0) { for (var backupIndex in automaticBackups) { - updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID) + updateOrAddTableRow(automaticBackups[backupIndex], AUTOMATIC_ARCHIVES_TBODY_ID); + } } if (manualBackups.length > 0) { for (var backupIndex in manualBackups) { - updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID) + updateOrAddTableRow(manualBackups[backupIndex], MANUAL_ARCHIVES_TBODY_ID); } } + // at this point, any rows that no longer have the ACTIVE_BACKUP_ROW_CLASS + // are deleted backups, so we remove them from the table + $('tbody tr:not(.' + ACTIVE_BACKUP_ROW_CLASS + ')').remove(); + // check if the restore action on all rows should be enabled or disabled $('.' + BACKUP_RESTORE_LINK_CLASS).parent().toggleClass('disabled', data.status.isRecovering); @@ -204,12 +222,15 @@ $(document).ready(function(){ // update the progress bars for current restore status if (data.status.isRecovering) { - updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress); + updateProgressBars($('.recovery.progress-bar'), data.status.recoveryProgress * 100); } // tell bootstrap sortable to update for the new rows $.bootstrapSortable({ applyLast: true }); + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(true); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(false); + }).fail(function(){ // we've hit the very rare case where we couldn't load the list of backups from the domain server @@ -219,11 +240,8 @@ $(document).ready(function(){ // replace the content archives panel with a simple error message // stating that the user should reload the page - $('#' + Settings.CONTENT_ARCHIVES_PANEL_ID + ' .panel-body').html( - "
    " + - "There was a problem loading your list of automatic and manual content archives. Please reload the page to try again." + - "
    " - ); + $('#' + CONTENT_ARCHIVES_NORMAL_ID).toggle(false); + $('#' + CONTENT_ARCHIVES_ERROR_ID).toggle(true); }).always(function(){ // toggle showing or hiding the tables depending on if they have entries @@ -377,6 +395,6 @@ $(document).ready(function(){ reloadBackupInformation(); // setup a timer to reload them every 5 seconds - setTimeout(reloadBackupInformation(), 5000); + setInterval(reloadBackupInformation, 5000); }; }); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index a3f99facea..cd7155d9da 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -1356,7 +1356,7 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(const QString&, typeValue), + Q_ARG(QString, typeValue), Q_ARG(bool, isAuthenticated), Q_ARG(bool, includeDomainSettings), Q_ARG(bool, includeContentSettings), @@ -1374,6 +1374,7 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt // only enumerate the requested settings type (domain setting or content setting) QJsonArray* filteredDescriptionArray = &_descriptionArray; + if (includeDomainSettings && !includeContentSettings) { filteredDescriptionArray = &_domainSettingsDescription; } else if (includeContentSettings && !includeDomainSettings) { From 5dec3aba505a2ab8c3c210f19bb2441b457b1f73 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:49:57 -0800 Subject: [PATCH 074/157] fix download link and restore behaviour with pending --- domain-server/resources/web/content/js/content.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 5c2e134102..717f149760 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -126,7 +126,7 @@ $(document).ready(function(){ var BACKUP_DOWNLOAD_LINK_CLASS = 'download-backup'; var BACKUP_DELETE_LINK_CLASS = 'delete-backup'; var ACTIVE_BACKUP_ROW_CLASS = 'active-backup'; - + function reloadBackupInformation() { // make a GET request to get backup information to populate the table $.ajax({ @@ -151,8 +151,8 @@ $(document).ready(function(){ + ""; + + "
  • Download
  • " + + "
  • Delete
  • "; } function updateProgressBars($progressBar, value) { @@ -267,12 +267,11 @@ $(document).ready(function(){ "Restore content", function() { // show a spinner while we send off our request - showSpinnerAlert("Restoring Content Archive " + backupName); + showSpinnerAlert("Starting restore of " + backupName); // setup an AJAX POST to request content restore $.post('/api/backups/recover/' + backupID).done(function(data, textStatus, jqXHR) { swal.close(); - showRestartModal(); }).fail(function(jqXHR, textStatus, errorThrown) { showErrorMessage( "Error", From 494f93304b9eb84c5815e5e9a50b1c0dd57d421e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 10:58:32 -0800 Subject: [PATCH 075/157] take down AssetClient after content manager --- domain-server/src/DomainServer.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 7cd6cd34fe..584cbe3513 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -380,11 +380,6 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; - // cleanup the AssetClient thread - DependencyManager::destroy(); - _assetClientThread.quit(); - _assetClientThread.wait(); - // destroy the LimitedNodeList before the DomainServer QCoreApplication is down DependencyManager::destroy(); @@ -392,6 +387,11 @@ DomainServer::~DomainServer() { _contentManager->aboutToFinish(); _contentManager->terminate(); } + + // cleanup the AssetClient thread + DependencyManager::destroy(); + _assetClientThread.quit(); + _assetClientThread.wait(); } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { From 441b55301f8637fa86f6c297266e4cd250f66252 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 11:01:22 -0800 Subject: [PATCH 076/157] cleanup LNL last during DS shutdown --- domain-server/src/DomainServer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 584cbe3513..5b8d253110 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -380,9 +380,6 @@ void DomainServer::parseCommandLine() { DomainServer::~DomainServer() { qInfo() << "Domain Server is shutting down."; - // destroy the LimitedNodeList before the DomainServer QCoreApplication is down - DependencyManager::destroy(); - if (_contentManager) { _contentManager->aboutToFinish(); _contentManager->terminate(); @@ -392,6 +389,9 @@ DomainServer::~DomainServer() { DependencyManager::destroy(); _assetClientThread.quit(); _assetClientThread.wait(); + + // destroy the LimitedNodeList before the DomainServer QCoreApplication is down + DependencyManager::destroy(); } void DomainServer::queuedQuit(QString quitMessage, int exitCode) { From 8e621a95a3c9abb5a0c4d948d60389f3b80d2533 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 11:04:58 -0800 Subject: [PATCH 077/157] fix typo in debug for writing new entities --- domain-server/src/DomainServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 5b8d253110..81dcf65be5 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1761,7 +1761,7 @@ void DomainServer::processOctreeDataPersistMessage(QSharedPointer Date: Fri, 16 Feb 2018 11:11:54 -0800 Subject: [PATCH 078/157] set DomainContentBackupManager object name so it appears on thread --- domain-server/src/DomainContentBackupManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index 40a2a55486..f6c6e7a7ba 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -57,6 +57,8 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire _persistInterval(persistInterval), _lastCheck(usecTimestampNow()) { + setObjectName("DomainContentBackupManager"); + // Make sure the backup directory exists. QDir(_backupDirectory).mkpath("."); From 1c053730eb997c5d0f1b037c882c9ab9bbae669d Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 14:09:00 -0800 Subject: [PATCH 079/157] make DomainServerSettingsManager thread-safe for use in content backup --- .../src/DomainContentBackupManager.cpp | 2 +- domain-server/src/DomainGatekeeper.cpp | 15 +- domain-server/src/DomainMetadata.cpp | 17 +- domain-server/src/DomainServer.cpp | 150 ++++++++---------- .../src/DomainServerSettingsManager.cpp | 95 +++++------ .../src/DomainServerSettingsManager.h | 26 +-- 6 files changed, 153 insertions(+), 152 deletions(-) diff --git a/domain-server/src/DomainContentBackupManager.cpp b/domain-server/src/DomainContentBackupManager.cpp index f6c6e7a7ba..0bef6bb891 100644 --- a/domain-server/src/DomainContentBackupManager.cpp +++ b/domain-server/src/DomainContentBackupManager.cpp @@ -58,7 +58,7 @@ DomainContentBackupManager::DomainContentBackupManager(const QString& backupDire _lastCheck(usecTimestampNow()) { setObjectName("DomainContentBackupManager"); - + // Make sure the backup directory exists. QDir(_backupDirectory).mkpath("."); diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index 3aab7b4563..e697bbdda1 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -435,10 +435,11 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect if (!userPerms.can(NodePermissions::Permission::canConnectPastMaxCapacity) && !isWithinMaxCapacity()) { // we can't allow this user to connect because we are at max capacity QString redirectOnMaxCapacity; - const QVariant* redirectOnMaxCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); - if (redirectOnMaxCapacityVariant && redirectOnMaxCapacityVariant->canConvert()) { - redirectOnMaxCapacity = redirectOnMaxCapacityVariant->toString(); + + QVariant redirectOnMaxCapacityVariant = + _server->_settingsManager.valueOrDefaultValueForKeyPath(MAXIMUM_USER_CAPACITY_REDIRECT_LOCATION); + if (redirectOnMaxCapacityVariant.canConvert()) { + redirectOnMaxCapacity = redirectOnMaxCapacityVariant.toString(); qDebug() << "Redirection domain:" << redirectOnMaxCapacity; } @@ -610,9 +611,9 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, bool DomainGatekeeper::isWithinMaxCapacity() { // find out what our maximum capacity is - const QVariant* maximumUserCapacityVariant = - valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY); - unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0; + QVariant maximumUserCapacityVariant = + _server->_settingsManager.valueOrDefaultValueForKeyPath(MAXIMUM_USER_CAPACITY); + unsigned int maximumUserCapacity = !maximumUserCapacityVariant.isValid() ? maximumUserCapacityVariant.toUInt() : 0; if (maximumUserCapacity > 0) { unsigned int connectedUsers = _server->countConnectedUsers(); diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index eee5673af3..24d55d74b6 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -84,21 +84,22 @@ void DomainMetadata::descriptorsChanged() { // get descriptors assert(_metadata[DESCRIPTORS].canConvert()); auto& state = *static_cast(_metadata[DESCRIPTORS].data()); - auto& settings = static_cast(parent())->_settingsManager.getSettingsMap(); - auto& descriptors = static_cast(parent())->_settingsManager.getDescriptorsMap(); + + static const QString DESCRIPTORS_GROUP_KEYPATH = "descriptors"; + auto descriptorsMap = static_cast(parent())->_settingsManager.valueForKeyPath(DESCRIPTORS).toMap(); // copy simple descriptors (description/maturity) - state[Descriptors::DESCRIPTION] = descriptors[Descriptors::DESCRIPTION]; - state[Descriptors::MATURITY] = descriptors[Descriptors::MATURITY]; + state[Descriptors::DESCRIPTION] = descriptorsMap[Descriptors::DESCRIPTION]; + state[Descriptors::MATURITY] = descriptorsMap[Descriptors::MATURITY]; // copy array descriptors (hosts/tags) - state[Descriptors::HOSTS] = descriptors[Descriptors::HOSTS].toList(); - state[Descriptors::TAGS] = descriptors[Descriptors::TAGS].toList(); + state[Descriptors::HOSTS] = descriptorsMap[Descriptors::HOSTS].toList(); + state[Descriptors::TAGS] = descriptorsMap[Descriptors::TAGS].toList(); // parse capacity static const QString CAPACITY = "security.maximum_user_capacity"; - const QVariant* capacityVariant = valueForKeyPath(settings, CAPACITY); - unsigned int capacity = capacityVariant ? capacityVariant->toUInt() : 0; + QVariant capacityVariant = static_cast(parent())->_settingsManager.valueForKeyPath(CAPACITY); + unsigned int capacity = capacityVariant.isValid() ? capacityVariant.toUInt() : 0; state[Descriptors::CAPACITY] = capacity; #if DEV_BUILD || PR_BUILD diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 81dcf65be5..9cecea5f70 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -75,8 +75,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, std::initializer_list optionalData, bool requireAccessToken) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant == nullptr && requireAccessToken) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid() && requireAccessToken) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -112,8 +112,8 @@ bool DomainServer::forwardMetaverseAPIRequest(HTTPConnection* connection, req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - if (accessTokenVariant != nullptr) { - auto accessTokenHeader = QString("Bearer ") + accessTokenVariant->toString(); + if (accessTokenVariant.isValid()) { + auto accessTokenHeader = QString("Bearer ") + accessTokenVariant.toString(); req.setRawHeader("Authorization", accessTokenHeader.toLatin1()); } @@ -417,8 +417,8 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() { const QString X509_PRIVATE_KEY_OPTION = "key"; const QString X509_KEY_PASSPHRASE_ENV = "DOMAIN_SERVER_KEY_PASSPHRASE"; - QString certPath = _settingsManager.getSettingsMap().value(X509_CERTIFICATE_OPTION).toString(); - QString keyPath = _settingsManager.getSettingsMap().value(X509_PRIVATE_KEY_OPTION).toString(); + QString certPath = _settingsManager.valueForKeyPath(X509_CERTIFICATE_OPTION).toString(); + QString keyPath = _settingsManager.valueForKeyPath(X509_PRIVATE_KEY_OPTION).toString(); if (!certPath.isEmpty() && !keyPath.isEmpty()) { // the user wants to use the following cert and key for HTTPS @@ -461,8 +461,7 @@ bool DomainServer::optionallySetupOAuth() { const QString OAUTH_CLIENT_SECRET_ENV = "DOMAIN_SERVER_CLIENT_SECRET"; const QString REDIRECT_HOSTNAME_OPTION = "hostname"; - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - _oauthProviderURL = QUrl(settingsMap.value(OAUTH_PROVIDER_URL_OPTION).toString()); + _oauthProviderURL = QUrl(_settingsManager.valueForKeyPath(OAUTH_PROVIDER_URL_OPTION).toString()); // if we don't have an oauth provider URL then we default to the default node auth url if (_oauthProviderURL.isEmpty()) { @@ -472,9 +471,9 @@ bool DomainServer::optionallySetupOAuth() { auto accountManager = DependencyManager::get(); accountManager->setAuthURL(_oauthProviderURL); - _oauthClientID = settingsMap.value(OAUTH_CLIENT_ID_OPTION).toString(); + _oauthClientID = _settingsManager.valueForKeyPath(OAUTH_CLIENT_ID_OPTION).toString(); _oauthClientSecret = QProcessEnvironment::systemEnvironment().value(OAUTH_CLIENT_SECRET_ENV); - _hostname = settingsMap.value(REDIRECT_HOSTNAME_OPTION).toString(); + _hostname = _settingsManager.valueForKeyPath(REDIRECT_HOSTNAME_OPTION).toString(); if (!_oauthClientID.isEmpty()) { if (_oauthProviderURL.isEmpty() @@ -499,11 +498,11 @@ static const QString METAVERSE_DOMAIN_ID_KEY_PATH = "metaverse.id"; void DomainServer::getTemporaryName(bool force) { // check if we already have a domain ID - const QVariant* idValueVariant = valueForKeyPath(_settingsManager.getSettingsMap(), METAVERSE_DOMAIN_ID_KEY_PATH); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); qInfo() << "Requesting temporary domain name"; - if (idValueVariant) { - qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant->toString(); + if (idValueVariant.isValid()) { + qDebug() << "A domain ID is already present in domain-server settings:" << idValueVariant.toString(); if (force) { qDebug() << "Requesting temporary domain name to replace current ID:" << getID(); } else { @@ -543,9 +542,6 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) { auto settingsDocument = QJsonDocument::fromJson(newSettingsJSON.toUtf8()); _settingsManager.recurseJSONObjectAndOverwriteSettings(settingsDocument.object(), DomainSettings); - // store the new ID and auto networking setting on disk - _settingsManager.persistToFile(); - // store the new token to the account info auto accountManager = DependencyManager::get(); accountManager->setTemporaryDomain(id, key); @@ -647,8 +643,6 @@ void DomainServer::setupNodeListAndAssignments() { QVariant localPortValue = _settingsManager.valueOrDefaultValueForKeyPath(CUSTOM_LOCAL_PORT_OPTION); int domainServerPort = localPortValue.toInt(); - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - int domainServerDTLSPort = INVALID_PORT; if (_isUsingDTLS) { @@ -656,8 +650,9 @@ void DomainServer::setupNodeListAndAssignments() { const QString CUSTOM_DTLS_PORT_OPTION = "dtls-port"; - if (settingsMap.contains(CUSTOM_DTLS_PORT_OPTION)) { - domainServerDTLSPort = (unsigned short) settingsMap.value(CUSTOM_DTLS_PORT_OPTION).toUInt(); + auto dtlsPortVariant = _settingsManager.valueForKeyPath(CUSTOM_DTLS_PORT_OPTION); + if (dtlsPortVariant.isValid()) { + domainServerDTLSPort = (unsigned short) dtlsPortVariant.toUInt(); } } @@ -687,9 +682,9 @@ void DomainServer::setupNodeListAndAssignments() { nodeList->setSessionUUID(_overridingDomainID); isMetaverseDomain = true; // assume metaverse domain } else { - const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH); - if (idValueVariant) { - nodeList->setSessionUUID(idValueVariant->toString()); + QVariant idValueVariant = _settingsManager.valueForKeyPath(METAVERSE_DOMAIN_ID_KEY_PATH); + if (idValueVariant.isValid()) { + nodeList->setSessionUUID(idValueVariant.toString()); isMetaverseDomain = true; // if we have an ID, we'll assume we're a metaverse domain } else { nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID @@ -758,10 +753,10 @@ bool DomainServer::resetAccountManagerAccessToken() { QString accessToken = QProcessEnvironment::systemEnvironment().value(ENV_ACCESS_TOKEN_KEY); if (accessToken.isEmpty()) { - const QVariant* accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); + QVariant accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); - if (accessTokenVariant && accessTokenVariant->canConvert(QMetaType::QString)) { - accessToken = accessTokenVariant->toString(); + if (accessTokenVariant.isValid() && accessTokenVariant.canConvert(QMetaType::QString)) { + accessToken = accessTokenVariant.toString(); } else { qWarning() << "No access token is present. Some operations that use the metaverse API will fail."; qDebug() << "Set an access token via the web interface, in your user config" @@ -892,31 +887,26 @@ void DomainServer::updateICEServerAddresses() { } void DomainServer::parseAssignmentConfigs(QSet& excludedTypes) { - const QString ASSIGNMENT_CONFIG_REGEX_STRING = "config-([\\d]+)"; - QRegExp assignmentConfigRegex(ASSIGNMENT_CONFIG_REGEX_STRING); - - const QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + const QString ASSIGNMENT_CONFIG_PREFIX = "config-"; // scan for assignment config keys - QStringList variantMapKeys = settingsMap.keys(); - int configIndex = variantMapKeys.indexOf(assignmentConfigRegex); + for (int i = 0; i < Assignment::AllTypes; ++i) { + QVariant assignmentConfigVariant = _settingsManager.valueOrDefaultValueForKeyPath(ASSIGNMENT_CONFIG_PREFIX + QString::number(i)); - while (configIndex != -1) { - // figure out which assignment type this matches - Assignment::Type assignmentType = (Assignment::Type) assignmentConfigRegex.cap(1).toInt(); + if (assignmentConfigVariant.isValid()) { + // figure out which assignment type this matches + Assignment::Type assignmentType = static_cast(i); - if (assignmentType < Assignment::AllTypes && !excludedTypes.contains(assignmentType)) { - QVariant mapValue = settingsMap[variantMapKeys[configIndex]]; - QVariantList assignmentList = mapValue.toList(); + if (!excludedTypes.contains(assignmentType)) { + QVariantList assignmentList = assignmentConfigVariant.toList(); - if (assignmentType != Assignment::AgentType) { - createStaticAssignmentsForType(assignmentType, assignmentList); + if (assignmentType != Assignment::AgentType) { + createStaticAssignmentsForType(assignmentType, assignmentList); + } + + excludedTypes.insert(assignmentType); } - - excludedTypes.insert(assignmentType); } - - configIndex = variantMapKeys.indexOf(assignmentConfigRegex, configIndex + 1); } } @@ -928,10 +918,10 @@ void DomainServer::addStaticAssignmentToAssignmentHash(Assignment* newAssignment void DomainServer::populateStaticScriptedAssignmentsFromSettings() { const QString PERSISTENT_SCRIPTS_KEY_PATH = "scripts.persistent_scripts"; - const QVariant* persistentScriptsVariant = valueForKeyPath(_settingsManager.getSettingsMap(), PERSISTENT_SCRIPTS_KEY_PATH); + QVariant persistentScriptsVariant = _settingsManager.valueOrDefaultValueForKeyPath(PERSISTENT_SCRIPTS_KEY_PATH); - if (persistentScriptsVariant) { - QVariantList persistentScriptsList = persistentScriptsVariant->toList(); + if (persistentScriptsVariant.isValid()) { + QVariantList persistentScriptsList = persistentScriptsVariant.toList(); foreach(const QVariant& persistentScriptVariant, persistentScriptsList) { QVariantMap persistentScript = persistentScriptVariant.toMap(); @@ -1954,13 +1944,12 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url auto nodeList = DependencyManager::get(); - auto getSetting = [this](QString keyPath, QVariant& value) -> bool { - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); - QVariant* var = valueForKeyPath(settingsMap, keyPath); - if (var == nullptr) { + auto getSetting = [this](QString keyPath, QVariant value) -> bool { + + value = _settingsManager.valueForKeyPath(keyPath); + if (!value.isValid()) { return false; } - value = *var; return true; }; @@ -2028,8 +2017,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url if (connection->requestOperation() == QNetworkAccessManager::GetOperation) { const QString URI_WIZARD = "/wizard/"; const QString WIZARD_COMPLETED_ONCE_KEY_PATH = "wizard.completed_once"; - const QVariant* wizardCompletedOnce = valueForKeyPath(_settingsManager.getSettingsMap(), WIZARD_COMPLETED_ONCE_KEY_PATH); - const bool completedOnce = wizardCompletedOnce && wizardCompletedOnce->toBool(); + QVariant wizardCompletedOnce = _settingsManager.valueForKeyPath(WIZARD_COMPLETED_ONCE_KEY_PATH); + const bool completedOnce = wizardCompletedOnce.isValid() && wizardCompletedOnce.toBool(); if (url.path() != URI_WIZARD && url.path().endsWith('/') && !completedOnce) { // First visit, redirect to the wizard @@ -2326,8 +2315,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return true; } else if (url.path() == "/domain_settings") { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400); return true; } @@ -2360,8 +2349,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url return forwardMetaverseAPIRequest(connection, "/api/v1/domains/" + domainID, "domain", { }, { "network_address", "network_port", "label" }); } else if (url.path() == URI_API_PLACES) { - auto accessTokenVariant = valueForKeyPath(_settingsManager.getSettingsMap(), ACCESS_TOKEN_KEY_PATH); - if (!accessTokenVariant->isValid()) { + auto accessTokenVariant = _settingsManager.valueForKeyPath(ACCESS_TOKEN_KEY_PATH); + if (!accessTokenVariant.isValid()) { connection->respond(HTTPConnection::StatusCode400, "User access token has not been set"); return true; } @@ -2409,7 +2398,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url QUrl url { NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/api/v1/places/" + place_id }; - url.setQuery("access_token=" + accessTokenVariant->toString()); + url.setQuery("access_token=" + accessTokenVariant.toString()); QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); @@ -2604,10 +2593,11 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl const QByteArray UNAUTHENTICATED_BODY = "You do not have permission to access this domain-server."; - QVariantMap& settingsMap = _settingsManager.getSettingsMap(); + QVariant adminUsersVariant = _settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY); + QVariant adminRolesVariant = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY); if (!_oauthProviderURL.isEmpty() - && (settingsMap.contains(ADMIN_USERS_CONFIG_KEY) || settingsMap.contains(ADMIN_ROLES_CONFIG_KEY))) { + && (adminUsersVariant.isValid() || adminRolesVariant.isValid())) { QString cookieString = connection->requestHeaders().value(HTTP_COOKIE_HEADER_KEY); const QString COOKIE_UUID_REGEX_STRING = HIFI_SESSION_COOKIE_KEY + "=([\\d\\w-]+)($|;)"; @@ -2618,7 +2608,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl cookieUUID = cookieUUIDRegex.cap(1); } - if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { qDebug() << "Config file contains web admin settings for OAuth and basic HTTP authentication." << "These cannot be combined - using OAuth for authentication."; } @@ -2628,13 +2618,13 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl DomainServerWebSessionData sessionData = _cookieSessionHash.value(cookieUUID); QString profileUsername = sessionData.getUsername(); - if (settingsMap.value(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { + if (_settingsManager.valueForKeyPath(ADMIN_USERS_CONFIG_KEY).toStringList().contains(profileUsername)) { // this is an authenticated user return true; } // loop the roles of this user and see if they are in the admin-roles array - QStringList adminRolesArray = settingsMap.value(ADMIN_ROLES_CONFIG_KEY).toStringList(); + QStringList adminRolesArray = _settingsManager.valueForKeyPath(ADMIN_ROLES_CONFIG_KEY).toStringList(); if (!adminRolesArray.isEmpty()) { foreach(const QString& userRole, sessionData.getRoles()) { @@ -2679,7 +2669,7 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl // we don't know about this user yet, so they are not yet authenticated return false; } - } else if (valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)) { + } else if (_settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).isValid()) { // config file contains username and password combinations for basic auth const QByteArray BASIC_AUTH_HEADER_KEY = "Authorization"; @@ -2698,10 +2688,10 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl QString headerPassword = credentialList[1]; // we've pulled a username and password - now check if there is a match in our basic auth hash - QString settingsUsername = valueForKeyPath(settingsMap, BASIC_AUTH_USERNAME_KEY_PATH)->toString(); - const QVariant* settingsPasswordVariant = valueForKeyPath(settingsMap, BASIC_AUTH_PASSWORD_KEY_PATH); + QString settingsUsername = _settingsManager.valueForKeyPath(BASIC_AUTH_USERNAME_KEY_PATH).toString(); + QVariant settingsPasswordVariant = _settingsManager.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); - QString settingsPassword = settingsPasswordVariant ? settingsPasswordVariant->toString() : ""; + QString settingsPassword = settingsPasswordVariant.isValid() ? settingsPasswordVariant.toString() : ""; QString hexHeaderPassword = headerPassword.isEmpty() ? "" : QCryptographicHash::hash(headerPassword.toUtf8(), QCryptographicHash::Sha256).toHex(); @@ -2838,13 +2828,14 @@ ReplicationServerInfo serverInformationFromSettings(QVariantMap serverMap, Repli } void DomainServer::updateReplicationNodes(ReplicationServerDirection direction) { - auto settings = _settingsManager.getSettingsMap(); - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { + auto broadcastSettingsVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + + if (broadcastSettingsVariant.isValid()) { auto nodeList = DependencyManager::get(); std::vector replicationNodesInSettings; - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + auto replicationSettings = broadcastSettingsVariant.toMap(); QString serversKey = direction == Upstream ? "upstream_servers" : "downstream_servers"; QString replicationDirection = direction == Upstream ? "upstream" : "downstream"; @@ -2920,13 +2911,12 @@ void DomainServer::updateUpstreamNodes() { void DomainServer::updateReplicatedNodes() { // Make sure we have downstream nodes in our list - auto settings = _settingsManager.getSettingsMap(); - static const QString REPLICATED_USERS_KEY = "users"; _replicatedUsernames.clear(); - - if (settings.contains(BROADCASTING_SETTINGS_KEY)) { - auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap(); + + auto replicationVariant = _settingsManager.valueForKeyPath(BROADCASTING_SETTINGS_KEY); + if (replicationVariant.isValid()) { + auto replicationSettings = replicationVariant.toMap(); if (replicationSettings.contains(REPLICATED_USERS_KEY)) { auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList(); for (auto& username : usersSettings) { @@ -3114,17 +3104,17 @@ void DomainServer::processPathQueryPacket(QSharedPointer messag // check out paths in the _configMap to see if we have a match auto keypath = QString(PATHS_SETTINGS_KEYPATH_FORMAT).arg(SETTINGS_PATHS_KEY).arg(pathQuery); - const QVariant* pathMatch = valueForKeyPath(_settingsManager.getSettingsMap(), keypath); + QVariant pathMatch = _settingsManager.valueForKeyPath(keypath); - if (pathMatch || pathQuery == INDEX_PATH) { + if (pathMatch.isValid() || pathQuery == INDEX_PATH) { // we got a match, respond with the resulting viewpoint auto nodeList = DependencyManager::get(); QString responseViewpoint; // if we didn't match the path BUT this is for the index path then send back our default - if (pathMatch) { - responseViewpoint = pathMatch->toMap()[PATH_VIEWPOINT_KEY].toString(); + if (pathMatch.isValid()) { + responseViewpoint = pathMatch.toMap()[PATH_VIEWPOINT_KEY].toString(); } else { const QString DEFAULT_INDEX_PATH = "/0,0,0/0,0,0,1"; responseViewpoint = DEFAULT_INDEX_PATH; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index cd7155d9da..ad0381a697 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -38,6 +38,9 @@ #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; +const QString SETTINGS_PATH = "/settings"; +const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; +const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; @@ -190,6 +193,9 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer(getSettingsMap()[DESCRIPTORS].data()); -} - void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows, QString groupName, NodePermissionsPointer perms) { // this is called when someone has used the domain-settings webpage to add a group. They type the group's name @@ -487,6 +482,9 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& void DomainServerSettingsManager::packPermissionsForMap(QString mapName, NodePermissionsMap& permissionsRows, QString keyPath) { + // grab a write lock on the settings mutex since we're about to change the config map + QWriteLocker locker(&_settingsLock); + // find (or create) the "security" section of the settings map QVariant* security = _configMap.valueForKeyPath("security", true); if (!security->canConvert(QMetaType::QVariantMap)) { @@ -576,15 +574,15 @@ bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& key mapPointer->clear(); - QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); - if (!permissions->canConvert(QMetaType::QVariantList)) { + QVariant permissions = valueForKeyPath(keyPath); + + if (!permissions.canConvert(QMetaType::QVariantList)) { qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings."; - (*permissions) = QVariantList(); } bool needPack = false; - QList permissionsList = permissions->toList(); + QList permissionsList = permissions.toList(); foreach (QVariant permsHash, permissionsList) { NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) }; QString id = perms->getID(); @@ -1068,12 +1066,22 @@ NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid& return getForbiddensForGroup(groupKey.first, groupKey.second); } +QVariant DomainServerSettingsManager::valueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); + auto foundValue = _configMap.valueForKeyPath(keyPath); + return foundValue ? *foundValue : QVariant(); +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { + QReadLocker locker(&_settingsLock); const QVariant* foundValue = _configMap.valueForKeyPath(keyPath); if (foundValue) { return *foundValue; } else { + // we don't need the settings lock anymore since we're done reading from the config map + _settingsLock.unlock(); + int dotIndex = keyPath.indexOf('.'); QString groupKey = keyPath.mid(0, dotIndex); @@ -1112,9 +1120,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection // we recurse one level deep below each group for the appropriate setting bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject, endpointType); - // store whatever the current _settingsMap is to file - persistToFile(); - // return success to the caller QString jsonSuccess = "{\"status\": \"success\"}"; connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); @@ -1216,16 +1221,9 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection } bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType) { - - if (thread() != QThread::currentThread()) { - bool success; - BLOCKING_INVOKE_METHOD(this, "restoreSettingsFromObject", - Q_RETURN_ARG(bool, success), - Q_ARG(QJsonObject, settingsToRestore), - Q_ARG(SettingsType, settingsType)); - return success; - } + // grab a write lock since we're about to change the settings map + QWriteLocker locker(&_settingsLock); QJsonArray* filteredDescriptionArray = settingsType == DomainSettings ? &_domainSettingsDescription : &_contentSettingsDescription; @@ -1341,6 +1339,10 @@ bool DomainServerSettingsManager::restoreSettingsFromObject(QJsonObject settings } else { // restore completed, persist the new settings qDebug() << "Restore completed, persisting restored settings to file"; + + // let go of the write lock since we're done making changes to the config map + locker.unlock(); + persistToFile(); return true; } @@ -1352,20 +1354,6 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt bool includeDefaults, bool isForBackup) { QJsonObject responseObject; - if (thread() != QThread::currentThread()) { - - BLOCKING_INVOKE_METHOD(this, "settingsResponseObjectForType", - Q_RETURN_ARG(QJsonObject, responseObject), - Q_ARG(QString, typeValue), - Q_ARG(bool, isAuthenticated), - Q_ARG(bool, includeDomainSettings), - Q_ARG(bool, includeContentSettings), - Q_ARG(bool, includeDefaults), - Q_ARG(bool, isForBackup)); - - return responseObject; - } - if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); @@ -1414,21 +1402,21 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt QVariant variantValue; if (!groupKey.isEmpty()) { - QVariant settingsMapGroupValue = _configMap.value(groupKey); + QVariant settingsMapGroupValue = valueForKeyPath(groupKey); if (!settingsMapGroupValue.isNull()) { variantValue = settingsMapGroupValue.toMap().value(settingName); } } else { - variantValue = _configMap.value(settingName); + variantValue = valueForKeyPath(settingName); } // final check for inclusion // either we include default values or we don't but this isn't a default value - if (includeDefaults || !variantValue.isNull()) { + if (includeDefaults || variantValue.isValid()) { QJsonValue result; - if (variantValue.isNull()) { + if (!variantValue.isValid()) { // no value for this setting, pass the default if (settingObject.contains(SETTING_DEFAULT_KEY)) { result = settingObject[SETTING_DEFAULT_KEY]; @@ -1567,6 +1555,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType) { + + // take a write lock since we're about to overwrite settings in the config map + QWriteLocker locker(&_settingsLock); + static const QString SECURITY_ROOT_KEY = "security"; static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist"; static const QString BROADCASTING_KEY = "broadcasting"; @@ -1664,6 +1656,12 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ } } + // we're done making changes to the config map, let go of our read lock + locker.unlock(); + + // store whatever the current config map is to file + persistToFile(); + return needRestart; } @@ -1690,6 +1688,9 @@ bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { } void DomainServerSettingsManager::sortPermissions() { + // take a write lock since we're about to change the config map data + QWriteLocker locker(&_settingsLock); + // sort the permission-names QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH); if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { @@ -1726,11 +1727,15 @@ void DomainServerSettingsManager::persistToFile() { QFile settingsFile(_configMap.getUserConfigFilename()); if (settingsFile.open(QIODevice::WriteOnly)) { + // take a read lock so we can grab the config and write it to file + QReadLocker locker(&_settingsLock); settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson()); } else { qCritical("Could not write to JSON settings file. Unable to persist settings."); // failed to write, reload whatever the current config state is + // with a write lock since we're about to overwrite the config map + QWriteLocker locker(&_settingsLock); _configMap.loadConfig(_argumentList); } } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 897a15485f..d81547410b 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -27,9 +27,6 @@ const QString SETTINGS_PATHS_KEY = "paths"; -const QString SETTINGS_PATH = "/settings"; -const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; -const QString CONTENT_SETTINGS_PATH_JSON = "/content-settings.json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; @@ -53,11 +50,12 @@ public: bool handleAuthenticatedHTTPRequest(HTTPConnection* connection, const QUrl& url); void setupConfigMap(const QStringList& argumentList); + + // each of the three methods in this group takes a read lock of _settingsLock + // and cannot be called when the a write lock is held by the same thread QVariant valueOrDefaultValueForKeyPath(const QString& keyPath); - - QVariantMap& getSettingsMap() { return _configMap.getConfig(); } - - QVariantMap& getDescriptorsMap(); + QVariant valueForKeyPath(const QString& keyPath); + bool containsKeyPath(const QString& keyPath) { return valueForKeyPath(keyPath).isValid(); } // these give access to anonymous/localhost/logged-in settings from the domain-server settings page bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); } @@ -119,6 +117,8 @@ public: /// thread safe method to restore settings from a JSON object Q_INVOKABLE bool restoreSettingsFromObject(QJsonObject settingsToRestore, SettingsType settingsType); + bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); + signals: void updateNodePermissions(); void settingsUpdated(); @@ -138,12 +138,13 @@ private: QStringList _argumentList; QJsonArray filteredDescriptionArray(bool isContentSettings); - bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, SettingsType settingsType); - void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription); QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName); void sortPermissions(); + + // you cannot be holding the _settingsLock when persisting to file from the same thread + // since it may take either a read lock or write lock and recursive locking doesn't allow a change in type void persistToFile(); void splitSettingsDescription(); @@ -155,10 +156,10 @@ private: QJsonArray _contentSettingsDescription; QJsonObject _settingsMenuGroups; + // any method that calls _valueForKeyPath on this _configMap must get a write lock it keeps until it + // is done with the returned QVariant* HifiConfigVariantMap _configMap; - friend class DomainServer; - // these cause calls to metaverse's group api void apiGetGroupID(const QString& groupName); void apiGetGroupRanks(const QUuid& groupID); @@ -192,6 +193,9 @@ private: // keep track of answers to api queries about which users are in which groups QHash> _groupMembership; // QHash> + + /// guard read/write access from multiple threads to settings + QReadWriteLock _settingsLock { QReadWriteLock::Recursive }; }; #endif // hifi_DomainServerSettingsManager_h From 679513599cce5f1b7a31b9312e3198c46bede1d2 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Fri, 16 Feb 2018 14:09:14 -0800 Subject: [PATCH 080/157] fix row hiding and paste events for badging --- domain-server/resources/web/content/js/content.js | 4 ++-- domain-server/resources/web/js/base-settings.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/domain-server/resources/web/content/js/content.js b/domain-server/resources/web/content/js/content.js index 717f149760..525b989259 100644 --- a/domain-server/resources/web/content/js/content.js +++ b/domain-server/resources/web/content/js/content.js @@ -145,7 +145,7 @@ $(document).ready(function(){ // populate the backups tables with the backups function createBackupTableRow(backup) { return "" - + "" + backup.name + "" + + "" + backup.name + "" + moment(backup.createdAtMillis).format('lll') + "" + "