Merge pull request #12409 from birarda/feat/content-archives-tables

Add content archive tables to DS content page
This commit is contained in:
Clément Brisset 2018-02-16 15:56:16 -08:00 committed by GitHub
commit b05c576fc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 781 additions and 204 deletions

View file

@ -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",

View file

@ -3,5 +3,4 @@
<script src='/js/bootbox.min.js'></script>
<script src='/js/form2js.min.js'></script>
<script src='/js/bootstrap-switch.min.js'></script>
<script src='/js/shared.js'></script>
<script src='/js/base-settings.js'></script>

View file

@ -14,6 +14,8 @@
<!--#include virtual="base-settings-scripts.html"-->
<script src="js/moment-locale.min.js"></script>
<script src="js/bootstrap-sortable.min.js"></script>
<script src="js/content.js"></script>
<!--#include virtual="page-end.html"-->

File diff suppressed because one or more lines are too long

View file

@ -1,37 +1,327 @@
$(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.</br>Verify that the file is a <i>.json</i> or <i>.json.gz</i> entities file and try again.',
html: true,
confirmButtonText: 'OK',
function setupBackupUpload() {
// construct the HTML needed for the settings backup panel
var html = "<div class='form-group'>";
html += "<span class='help-block'>Upload a content archive (.zip) or entity file (.json, .json.gz) to replace the content of this domain.";
html += "<br/>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.</span>";
html += "<input id='restore-settings-file' name='restore-settings' type='file'>";
html += "<button type='button' id='" + RESTORE_SETTINGS_UPLOAD_ID + "' disabled='true' class='btn btn-primary'>Upload Content</button>";
html += "</div>";
$('#' + 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();
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]);
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."
);
});
}
});
ev.preventDefault();
showSpinnerAlert("Uploading Entities File");
);
});
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 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 = "<div class='form-group'>";
html += "<label class='control-label'>Automatic Content Archives</label>";
html += "<span class='help-block'>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 += "<a href='/settings/#automatic_content_archives' id='" + AUTO_ARCHIVES_SETTINGS_LINK_ID + "'>Click here to manage automatic content archive intervals.</a>";
html += "</div>";
html += "<table class='table sortable' id='" + AUTOMATIC_ARCHIVES_TABLE_ID + "'>";
var backups_table_head = "<thead><tr class='gray-tr'><th>Archive Name</th><th data-defaultsort='desc'>Archive Date</th><th class='text-right' data-defaultsort='disabled'>Actions</th></tr></thead>";
html += backups_table_head;
html += "<tbody id='" + AUTOMATIC_ARCHIVES_TBODY_ID + "'></tbody></table>";
html += "<div class='form-group'>";
html += "<label class='control-label'>Manual Content Archives</label>";
html += "<span class='help-block'>You can generate and download an archive of your domain content right now. You can also download, delete and restore any archive listed.</span>";
html += "<button type='button' id='" + GENERATE_ARCHIVE_BUTTON_ID + "' class='btn btn-primary'>Generate New Archive</button>";
html += "</div>";
html += "<table class='table sortable' id='" + MANUAL_ARCHIVES_TABLE_ID + "'>";
html += backups_table_head;
html += "<tbody id='" + MANUAL_ARCHIVES_TBODY_ID + "'></tbody></table>";
// put the base HTML in the content archives panel
$('#' + 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
$.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;
});
manualBackups = splitBackups[0];
automaticBackups = splitBackups[1];
// populate the backups tables with the backups
function createBackupTableRow(backup) {
return "<tr data-backup-id='" + backup.id + "' data-backup-name='" + backup.name + "'>"
+ "<td data-value='" + backup.name.toLowerCase() + "'>" + backup.name + "</td><td data-dateformat='lll'>"
+ moment(backup.createdAtMillis).format('lll')
+ "</td><td class='text-right'>"
+ "<div class='dropdown'><div class='dropdown-toggle' data-toggle='dropdown' aria-expanded='false'><span class='glyphicon glyphicon-option-vertical'></span></div>"
+ "<ul class='dropdown-menu dropdown-menu-right'>"
+ "<li><a class='" + BACKUP_RESTORE_LINK_CLASS + "' href='#'>Restore from here</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DOWNLOAD_LINK_CLASS + "' href='#'>Download</a></li><li class='divider'></li>"
+ "<li><a class='" + BACKUP_DELETE_LINK_CLASS + "' href='/api/backups/" + backup.id + "' target='_blank'>Delete</a></li></ul></div></td>";
}
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(
"<div class='form-group'>" +
"<span class='help-block'>There was a problem loading your list of automatic and manual content archives. Please reload the page to try again.</span>" +
"</div>"
);
}).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);
});
}
// 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
reloadLatestBackups();
});
}
)
});
// 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');
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;
}
);
}
});
// handle click on manual archive creation button
$('body').on('click', '#' + GENERATE_ARCHIVE_BUTTON_ID, function(e) {
e.preventDefault();
// 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/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
reloadLatestBackups();
}).fail(function(jqXHR, textStatus, errorThrown) {
});
swal.close();
});
});
Settings.extraGroupsAtIndex = Settings.extraContentGroupsAtIndex;
Settings.afterReloadActions = function() {
setupBackupUpload();
setupContentArchives();
// load the latest backups immediately
reloadLatestBackups();
};
});

File diff suppressed because one or more lines are too long

View file

@ -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;
}

View file

@ -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;
}

View file

@ -1,4 +1,5 @@
</div>
<script src='/js/jquery-2.1.4.min.js'></script>
<script src='/js/bootstrap.min.js'></script>
<script src='/js/shared.js'></script>
<script src='/js/domain-server.js'></script>

View file

@ -9,6 +9,7 @@
<link href="/css/style.css" rel="stylesheet" media="screen">
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-sortable.css" rel="stylesheet" media="screen">
<script src='/js/sweetalert.min.js'></script>
</head>

View file

@ -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));
@ -122,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() {
@ -801,6 +807,8 @@ function badgeForDifferences(changedElement) {
}
});
Settings.pendingChanges = totalChanges;
if (totalChanges == 0) {
totalChanges = ""
}

View file

@ -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');
@ -39,22 +39,49 @@ $(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;
});
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 / Restore'
}
]
// 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 +92,15 @@ $(document).ready(function(){
return "<li class='setting-group'><a href='" + settingsGroupAnchor(base, html_id) + "'>" + group.label + "<span class='badge'></span></a></li>";
}
// 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.extraContentGroupsAtEnd[endIndex]);
}
$.each(data.content_settings, function(index, group){
if (index > 0) {
$contentDropdown.append("<li role='separator' class='divider'></li>");
@ -73,25 +109,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("<li role='separator' class='divider'></li>");
}
$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("<li role='separator' class='divider'></li>");
$settingsDropdown.append(makeGroupDropdownElement({ html_id: 'places', label: 'Places' }, "/settings/"));
}
});
// append a link for the "Settings Backup" panel
$settingsDropdown.append("<li role='separator' class='divider'></li>");
$settingsDropdown.append(makeGroupDropdownElement({ html_id: 'settings_backup', label: 'Settings Backup'}, "/settings"));
});
}
});

View file

@ -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 = {
@ -96,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') {
@ -164,7 +177,7 @@ function getDomainFromAPI(callback) {
if (callback === undefined) {
callback = function() {};
}
if (!domainIDIsSet()) {
callback({ status: 'fail' });
return null;

View file

@ -14,17 +14,8 @@ $(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
@ -103,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;
}
@ -643,7 +631,6 @@ $(document).ready(function(){
autoNetworkingEl.after(form);
}
function setupPlacesTable() {
// create a dummy table using our view helper
var placesTableSetting = {
@ -1043,32 +1030,38 @@ $(document).ready(function(){
$('body').on('click', '#' + RESTORE_SETTINGS_UPLOAD_ID, function(e){
e.preventDefault();
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() {
@ -1089,7 +1082,7 @@ $(document).ready(function(){
html += "<div class='form-group'>";
html += "<label class='control-label'>Upload a Settings Configuration</label>";
html += "<span class='help-block'>Upload a settings configuration to quickly configure this domain";
html += "<br/>Note: Your domain's settings will be replaced by the settings you upload</span>";
html += "<br/>Note: Your domain settings will be replaced by the settings you upload</span>";
html += "<input id='restore-settings-file' name='restore-settings' type='file'>";
html += "<button type='button' id='" + RESTORE_SETTINGS_UPLOAD_ID + "' disabled='true' class='btn btn-primary'>Upload Domain Settings</button>";
@ -1097,8 +1090,5 @@ $(document).ready(function(){
html += "</div>";
$('#settings_backup .panel-body').html(html);
// add an upload button to the footer to kick off the upload form
}
});

View file

@ -261,6 +261,5 @@
<script src='/js/underscore-min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='/js/sha256.js'></script>
<script src='/js/shared.js'></script>
<script src='js/wizard.js'></script>
<!--#include virtual="page-end.html"-->

View file

@ -18,6 +18,7 @@
#include <fstream>
#include <time.h>
#include <QBuffer>
#include <QDateTime>
#include <QDebug>
#include <QDir>
@ -63,9 +64,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) {
@ -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<BackupItemInfo> DomainContentBackupManager::getAllBackups() {
QDir backupDir { _backupDirectory };
auto matchingFiles =
backupDir.entryInfoList({ AUTOMATIC_BACKUP_PREFIX + "*.zip", MANUAL_BACKUP_PREFIX + "*.zip" },

View file

@ -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<bool, QString> createBackup(const QString& prefix, const QString& name);
bool recoverFromBackupZip(QuaZip& backupZip, const QString& backupName);
private:
const QString _backupDirectory;
std::vector<BackupHandlerPointer> _backupHandlers;

View file

@ -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<QVariantMap>()) {
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())));
@ -1934,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";
@ -2244,17 +2251,52 @@ 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> 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 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);
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, 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);
}
// respond with a 200 for success
connection->respond(HTTPConnection::StatusCode200);
} else {
// respond with a 400 for failure
connection->respond(HTTPConnection::StatusCode400);

View file

@ -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_GROUP + ".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;

View file

@ -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<QUuid, QUuid>; // groupID, rankID