mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
45aacb97c8
Import uploads broke
1236 lines
41 KiB
HTML
1236 lines
41 KiB
HTML
{% extends ../base.html %}
|
|
|
|
{% block meta %}
|
|
{% end %}
|
|
|
|
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="content-wrapper">
|
|
|
|
<!-- Page Title Header Starts-->
|
|
<div class="row page-title-header">
|
|
<div class="col-12">
|
|
<div class="page-header">
|
|
<h4 class="page-title">
|
|
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
|
|
data['server_stats']['server_id']['server_name'] }}
|
|
<br />
|
|
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
|
|
</h4>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
<!-- Page Title Header Ends-->
|
|
|
|
{% include "parts/details_stats.html" %}
|
|
|
|
<div class="row">
|
|
|
|
<div class="col-sm-12 grid-margin">
|
|
<div class="card">
|
|
<div class="card-body pt-0">
|
|
<span class="d-none d-sm-block">
|
|
{% include "parts/server_controls_list.html %}
|
|
</span>
|
|
<span class="d-block d-sm-none">
|
|
{% include "parts/m_server_controls_list.html %}
|
|
</span>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 col-sm-12">
|
|
<noscript>
|
|
{{ translate('serverFiles', 'noscript', data['lang']) }}
|
|
</noscript>
|
|
<div id="files-tree-nav" class="overlay" style="background-color: #9f9daf !important;">
|
|
|
|
<!-- Button to close the overlay navigation -->
|
|
|
|
<!-- Overlay content -->
|
|
<div id="files-tree-nav-content" class="overlay-content" style="background-color: #9f9daf;">
|
|
<h4 id="context-title" style="color:#4b00ff; text-align: center; padding: 3px;">
|
|
</h4>
|
|
<p style="width: 90%; border-bottom: 2px solid black; margin:0 auto;"></p>
|
|
<a onclick="createFileE(event)" href="javascript:void(0)" id="createFile" href="#">{{
|
|
translate('serverFiles', 'createFile', data['lang']) }}</a>
|
|
<a onclick="createDirE(event)" href="javascript:void(0)" id="createDir" href="#">{{
|
|
translate('serverFiles', 'createDir', data['lang']) }}</a>
|
|
<a onclick="renameItemE(event)" href="javascript:void(0)" id="renameItem" href="#">{{
|
|
translate('serverFiles', 'rename', data['lang']) }}</a>
|
|
<a onclick="uploadFilesE(event)" href="javascript:void(0)" id="upload" href="#">{{
|
|
translate('serverFiles', 'upload', data['lang']) }}</a>
|
|
<a onclick="unzipFilesE(event)" href="javascript:void(0)" id="unzip" href="#">{{
|
|
translate('serverFiles', 'unzip', data['lang']) }}</a>
|
|
<a onclick="downloadFileE(event)" href="javascript:void(0)" id="downloadFile" href="#">{{
|
|
translate('serverFiles', 'download', data['lang']) }}</a>
|
|
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
|
|
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
|
|
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#"
|
|
style="color: red">{{
|
|
translate('serverFiles', 'delete', data['lang']) }}</a>
|
|
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
|
|
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
|
|
translate('serverFiles', 'close', data['lang']) }}</a>
|
|
</div>
|
|
|
|
</div>
|
|
<style>
|
|
/* The Overlay (background) */
|
|
.overlay {
|
|
display: none;
|
|
flex-direction: column;
|
|
background-color: #9f9daf;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 20px rgb(64 64 64 / 5%);
|
|
padding: 10px 0;
|
|
z-index: 10000;
|
|
overflow: scroll;
|
|
|
|
}
|
|
|
|
.overlay::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* Hide scrollbar for IE, Edge and Firefox */
|
|
.overlay {
|
|
-ms-overflow-style: none;
|
|
/* IE and Edge */
|
|
scrollbar-width: none;
|
|
/* Firefox */
|
|
}
|
|
|
|
/* Position the content inside the overlay */
|
|
.overlay-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: #9f9daf;
|
|
border-radius: 10px;
|
|
box-shadow: 0 10px 20px rgb(64 64 64 / 5%);
|
|
padding: 10px 0;
|
|
}
|
|
|
|
/* The navigation links inside the overlay */
|
|
.overlay a {
|
|
font: inherit;
|
|
border: 0;
|
|
padding: 10px 30px 10px 15px;
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
position: relative;
|
|
text-decoration: unset;
|
|
color: #000;
|
|
font-weight: 500;
|
|
transition: 0.5s linear;
|
|
-webkit-transition: 0.5s linear;
|
|
-moz-transition: 0.5s linear;
|
|
-ms-transition: 0.5s linear;
|
|
-o-transition: 0.5s linear;
|
|
}
|
|
|
|
/* When you mouse over the navigation links, change their color */
|
|
.overlay a:hover,
|
|
.overlay a:focus {
|
|
background: grey;
|
|
color: #4b00ff;
|
|
}
|
|
|
|
/* Position the close button (top right corner) */
|
|
.overlay .closebtn .closebtn:hover {
|
|
background-color: red;
|
|
color: red;
|
|
z-index: 10000;
|
|
}
|
|
|
|
/* When the height of the screen is less than 450 pixels, change the font-size of the links and position the close button again, so they don't overlap */
|
|
@media screen and (max-height: 450px) {
|
|
.overlay a {
|
|
font-size: 20px
|
|
}
|
|
|
|
.overlay .closebtn {
|
|
font-size: 40px;
|
|
top: 15px;
|
|
right: 35px;
|
|
}
|
|
}
|
|
|
|
.tree-file:hover {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|
|
<ul class="tree-view">
|
|
<li>
|
|
<div id="root_dir" class="tree-ctx-item" data-path="{{ data['server_stats']['server_id']['path'] }}">
|
|
<span id="{{ data['server_stats']['server_id']['path'] }}span"
|
|
class="files-tree-title tree-caret-down root-dir"
|
|
data-path="{{ data['server_stats']['server_id']['path'] }}" onclick="getToggleMain(event)">
|
|
<i class="far fa-folder"></i>
|
|
<i class="far fa-folder-open"></i>
|
|
{{ translate('serverFiles', 'files', data['lang']) }}
|
|
</span>
|
|
</div>
|
|
<ul class="tree-nested d-block" id="files-tree">
|
|
<li><i class="fa fa-spin fa-spinner"></i>{{ translate('serverFiles', 'loadingRecords', data['lang'])
|
|
}}</li>
|
|
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<style>
|
|
/* Remove default bullets */
|
|
.tree-view,
|
|
.tree-nested {
|
|
list-style-type: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
/* Style the items */
|
|
.tree-item,
|
|
.files-tree-title {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
/* Prevent text selection */
|
|
}
|
|
|
|
/* Create the caret/arrow with a unicode, and style it */
|
|
.tree-caret .fa-folder {
|
|
display: inline-block;
|
|
}
|
|
|
|
.tree-caret .fa-folder-open {
|
|
display: none;
|
|
}
|
|
|
|
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
|
|
.tree-caret-down .fa-folder {
|
|
display: none;
|
|
}
|
|
|
|
.tree-caret-down .fa-folder-open {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Hide the nested list */
|
|
.tree-nested {
|
|
display: none;
|
|
}
|
|
|
|
html,
|
|
body,
|
|
body>.container-scroller {
|
|
overflow: initial;
|
|
}
|
|
|
|
.editorManager {
|
|
top: 63px;
|
|
position: sticky;
|
|
}
|
|
</style>
|
|
<div id="editor_container" class="col-md-6 col-sm-12">
|
|
<br>
|
|
<br>
|
|
<div class="editorManager">
|
|
<h2 id="fileError"></h2>
|
|
<div id="editorParent">
|
|
{{ translate('serverFiles', 'editingFile', data['lang']) }} <span id="editingFile"></span>
|
|
<div id="editor" onresize="editor.resize()" style="resize: both;width: 100%;">file_contents</div>
|
|
<br />
|
|
</div>
|
|
{{ translate('serverFiles', 'keybindings', data['lang']) }}:
|
|
<div class="btn-group" role="group">
|
|
<button onclick="setKeyboard(event.target)" class="btn btn-primary" data-handler-name="null">{{
|
|
translate('serverFiles', 'default', data['lang']) }}</button>
|
|
<button onclick="setKeyboard(event.target)" class="btn btn-secondary"
|
|
data-handler-name="ace/keyboard/vim">Vim</button>
|
|
<button onclick="setKeyboard(event.target)" class="btn btn-secondary"
|
|
data-handler-name="ace/keyboard/emacs">Emacs</button>
|
|
<button onclick="setKeyboard(event.target)" class="btn btn-secondary"
|
|
data-handler-name="ace/keyboard/sublime">Sublime</button>
|
|
<span class="d-none d-md-block "> <button class="btn btn-info" id="screen-size">{{
|
|
translate('serverFiles',
|
|
'size', data['lang']) }}</button></span>
|
|
</div>
|
|
<h3 id="file_warn"></h3>
|
|
<button class="btn btn-success" onclick="save()"><i class="fas fa-save"></i> {{ translate('serverFiles',
|
|
'save', data['lang']) }}</button>
|
|
<span style="color: #2fb689; margin-left: 10px;" id="save_status"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
<!-- content-wrapper ends -->
|
|
|
|
{% end %}
|
|
|
|
{% block js %}
|
|
<script src="/static/assets/vendors/ace-builds/src-min/ace.js" type="text/javascript" charset="utf-8"></script>
|
|
|
|
<script>
|
|
|
|
const serverId = new URLSearchParams(document.location.search).get('id')
|
|
|
|
|
|
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
|
function getCookie(name) {
|
|
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
|
return r ? r[1] : undefined;
|
|
}
|
|
|
|
|
|
let editor = ace.edit('editor');
|
|
editor.setTheme('ace/theme/dracula');
|
|
editor.session.setUseSoftTabs(true);
|
|
editor.commands.addCommand({
|
|
name: 'saveFile',
|
|
bindKey: {
|
|
win: 'Ctrl-S',
|
|
mac: 'Command-S',
|
|
sender: 'editor|cli'
|
|
},
|
|
exec: function (env, args, request) {
|
|
save()
|
|
}
|
|
});
|
|
|
|
// mouseup = css resize end
|
|
document.addEventListener("mouseup", function (e) {
|
|
editor.resize();
|
|
});
|
|
|
|
let extensionChanges = [
|
|
{
|
|
regex: /^js$/,
|
|
replaceWith: 'ace/mode/javascript'
|
|
},
|
|
{
|
|
regex: /^py$/,
|
|
replaceWith: 'ace/mode/python'
|
|
},
|
|
{
|
|
regex: /^html$/,
|
|
replaceWith: 'ace/mode/html'
|
|
},
|
|
{
|
|
regex: /^yml$/,
|
|
replaceWith: 'ace/mode/yaml'
|
|
},
|
|
{
|
|
regex: /^yaml$/,
|
|
replaceWith: 'ace/mode/yaml'
|
|
},
|
|
{
|
|
regex: /^txt$/,
|
|
replaceWith: 'ace/mode/text'
|
|
},
|
|
{
|
|
regex: /^json$/,
|
|
replaceWith: 'ace/mode/json'
|
|
},
|
|
{
|
|
regex: /^java$/,
|
|
replaceWith: 'ace/mode/java'
|
|
},
|
|
{
|
|
regex: /^cpp$/,
|
|
replaceWith: 'ace/mode/c_cpp'
|
|
},
|
|
{
|
|
regex: /^c$/,
|
|
replaceWith: 'ace/mode/c_cpp'
|
|
},
|
|
{
|
|
regex: /^css$/,
|
|
replaceWith: 'ace/mode/css'
|
|
},
|
|
{
|
|
regex: /^scss$/,
|
|
replaceWith: 'ace/mode/scss'
|
|
},
|
|
{
|
|
regex: /^sass$/,
|
|
replaceWith: 'ace/mode/sass'
|
|
},
|
|
{
|
|
regex: /^lua$/,
|
|
replaceWith: 'ace/mode/lua'
|
|
},
|
|
{
|
|
regex: /^php$/,
|
|
replaceWith: 'ace/mode/php'
|
|
},
|
|
{
|
|
regex: /^ps1$/,
|
|
replaceWith: 'ace/mode/powershell'
|
|
},
|
|
{
|
|
regex: /^svg$/,
|
|
replaceWith: 'ace/mode/svg'
|
|
},
|
|
{
|
|
regex: /^sh$/,
|
|
replaceWith: 'ace/mode/sh'
|
|
},
|
|
{
|
|
regex: /^xml$/,
|
|
replaceWith: 'ace/mode/xml'
|
|
},
|
|
{
|
|
regex: /^ts$/,
|
|
replaceWith: 'ace/mode/typescript'
|
|
},
|
|
{
|
|
regex: /^properties$/,
|
|
replaceWith: 'ace/mode/properties'
|
|
},
|
|
{
|
|
regex: /^log$/,
|
|
replaceWith: 'ace/mode/txt'
|
|
},
|
|
];
|
|
|
|
let path = '', serverFileContent = '';
|
|
|
|
async function clickOnFile(event) {
|
|
const token = getCookie("_xsrf");
|
|
path = event.target.getAttribute('data-path');
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "page": "files", "path": path }),
|
|
});
|
|
let responseData = await res.json();
|
|
console.log(responseData)
|
|
if (responseData.status === "ok") {
|
|
console.log('Got File Contents From Server');
|
|
$('#editorParent').toggle(true) // show
|
|
$('#fileError').toggle(false) // hide
|
|
setFileName(event.target.innerText);
|
|
editor.session.setValue(responseData.data);
|
|
serverFileContent = responseData.data;
|
|
setSaveStatus(true);
|
|
}
|
|
else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
function setFileName(name) {
|
|
let fileName = name || 'default.txt';
|
|
document.getElementById('editingFile').innerText = fileName;
|
|
|
|
if (fileName.match('.')) {
|
|
|
|
// The pop method removes and returns the last element.
|
|
setMode(fileName
|
|
.split('.')
|
|
.pop()
|
|
.replace('ace/mode/', ''));
|
|
|
|
} else {
|
|
setMode('txt');
|
|
document
|
|
.querySelector('#file_warn')
|
|
.innerText = "{% raw translate('serverFiles', 'unsupportedLanguage', data['lang']) %}";
|
|
}
|
|
}
|
|
|
|
var onlongtouch;
|
|
var timer;
|
|
var touchduration = 500; //length of time we want the user to touch before we do something
|
|
const longtouch = new Event('longtouch');
|
|
|
|
function touchstart(event) {
|
|
if (!timer) {
|
|
timer = setTimeout(onlongtouch, touchduration, event);
|
|
}
|
|
}
|
|
|
|
function touchend(event) {
|
|
//stops short touches from firing the event
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
console.log('Timer: ' + timer)
|
|
timer = null;
|
|
}
|
|
}
|
|
|
|
onlongtouch = function (e) {
|
|
console.log('iOS long touch detected!');
|
|
if ([
|
|
'iPad Simulator',
|
|
'iPhone Simulator',
|
|
'iPod Simulator',
|
|
'iPad',
|
|
'iPhone',
|
|
'iPod'
|
|
].includes(navigator.platform)
|
|
// iPad on iOS 13 detection
|
|
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)) {
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
var ctxmenuPath = e.target.getAttribute('data-path');
|
|
var ctxmenuName = e.target.getAttribute('data-name');
|
|
|
|
document.getElementById('context-title').innerHTML = ctxmenuName;
|
|
if (!ctxmenuPath) {
|
|
return;
|
|
}
|
|
$('#renameItem').show();
|
|
|
|
var isDir = e.target.classList.contains('files-tree-title');
|
|
$('#createFile').toggle(isDir);
|
|
$('#createDir').toggle(isDir);
|
|
$('#deleteDir').toggle(isDir);
|
|
$('#upload').toggle(isDir);
|
|
|
|
var isFile = e.target.classList.contains('tree-file');
|
|
$('#deleteFile').toggle(isFile);
|
|
$('#downloadFile').toggle(isFile);
|
|
|
|
if (e.target.classList.contains('root-dir')) {
|
|
$('#createFile').show();
|
|
$('#createDir').show();
|
|
$('#renameItem').hide();
|
|
$('#deleteDir').hide();
|
|
$('#deleteFile').hide();
|
|
$('#downloadFile').hide();
|
|
$('#upload').show();
|
|
}
|
|
if (e.target.textContent.endsWith('.zip')) {
|
|
$('#unzip').show();
|
|
} else {
|
|
$('#unzip').hide();
|
|
}
|
|
|
|
var clientX = e.layerX + 2;
|
|
var clientY = e.layerY + 5;
|
|
|
|
|
|
|
|
document.getElementById('files-tree-nav-content')
|
|
.setAttribute('data-path', ctxmenuPath);
|
|
document.getElementById('files-tree-nav-content')
|
|
.setAttribute('data-name', ctxmenuName);
|
|
document.getElementById("files-tree-nav").style.display = "flex";
|
|
document.getElementById("files-tree-nav").style.position = "fixed";
|
|
domRect = document.getElementById("files-tree-nav").getBoundingClientRect();
|
|
sum = (clientY + domRect['height']) - window.innerHeight
|
|
if (domRect['height'] + clientY > window.innerHeight) {
|
|
clientY = clientY - sum;
|
|
}
|
|
document.getElementById("files-tree-nav").style.top = clientY + 'px';
|
|
document.getElementById("files-tree-nav").style.left = clientX + 'px';
|
|
timer = null;
|
|
};
|
|
}
|
|
/**
|
|
* @param {boolean} saved
|
|
*/
|
|
const setSaveStatus = (saved) => {
|
|
document.getElementById('save_status').innerHTML = `<i class="${saved ? "fa-solid fa-file-circle-check" : "fa-regular fa-file"}"></i>`;
|
|
document.getElementById('save_status').style.color = saved ? '#2fb689' : 'gray';
|
|
}
|
|
['change', 'undo', 'redo'].forEach(event => editor.on(event, (event) => setSaveStatus(serverFileContent === editor.session.getValue())))
|
|
$('#screen-size').on('click', function (e) {
|
|
$('#editor_container').toggleClass(`col-md-6`)
|
|
|
|
});
|
|
setFileName();
|
|
$('#editorParent').toggle(false) // show
|
|
$('#fileError').toggle(false) // hide
|
|
editor.blur()
|
|
|
|
function setMode(extension) {
|
|
// if the extension matches with the RegEx it will return the replaceWith
|
|
// property. else it will return the one it has. defaults to the extension.
|
|
// this runs for each element in extensionChanges.
|
|
let aceMode = extensionChanges.reduce((output, element) => {
|
|
return extension.match(element.regex)
|
|
? element.replaceWith
|
|
: output;
|
|
}, extension);
|
|
|
|
if (!aceMode.startsWith('ace/mode/')) {
|
|
document
|
|
.querySelector('#file_warn')
|
|
.innerText = "{% raw translate('serverFiles', 'unsupportedLanguage', data['lang']) %}";
|
|
} else {
|
|
document
|
|
.querySelector('#file_warn')
|
|
.innerText = '';
|
|
|
|
console.log(aceMode || 'ace/mode/text');
|
|
editor.session.setMode(aceMode || 'ace/mode/text');
|
|
}
|
|
}
|
|
|
|
|
|
async function save() {
|
|
let text = editor.session.getValue();
|
|
|
|
const token = getCookie("_xsrf")
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "path": path, "contents": text }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
serverFileContent = text;
|
|
setSaveStatus(true)
|
|
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
async function createFile(parent, name, callback) {
|
|
const token = getCookie("_xsrf")
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "parent": parent, "name": name, "directory": false }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
getTreeView($('#root_dir').data('path'));
|
|
setTreeViewContext();
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
async function createDir(parent, name, callback) {
|
|
const token = getCookie("_xsrf")
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "parent": parent, "name": name, "directory": true }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
getTreeView($('#root_dir').data('path'));
|
|
setTreeViewContext();
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
async function renameItem(path, name, callback) {
|
|
const token = getCookie("_xsrf")
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files/create/`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "path": path, "new_name": name }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
getTreeView($('#root_dir').data('path'));
|
|
setTreeViewContext();
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
async function deleteItem(path, el, callback) {
|
|
const token = getCookie("_xsrf");
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "filename": path }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
el = document.getElementById(path + "li");
|
|
$(el).remove();
|
|
document.getElementById('files-tree-nav').style.display = 'none';
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
async function unZip(path, callback) {
|
|
const token = getCookie("_xsrf")
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files/zip/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "folder": path }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
getTreeView($('#root_dir').data('path'));
|
|
setTreeViewContext();
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
async function uploadFile(file, path, file_num, onProgress) {
|
|
const fileId = uuidv4();
|
|
const token = getCookie("_xsrf")
|
|
if (!file) {
|
|
alert("Please select a file first.");
|
|
return;
|
|
}
|
|
|
|
const chunkSize = 1024 * 1024; // 1MB
|
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
|
|
|
const uploadPromises = [];
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files/upload/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-XSRFToken': token,
|
|
'chunked': true,
|
|
'type': "server_upload",
|
|
'fileSize': file.size,
|
|
'total_chunks': totalChunks,
|
|
'location': path,
|
|
'filename': file.name,
|
|
'fileId': fileId,
|
|
},
|
|
body: null,
|
|
});
|
|
|
|
let responseData = await res.json();
|
|
|
|
let file_id = ""
|
|
if (responseData.status === "ok") {
|
|
file_id = responseData.data["file-id"]
|
|
}
|
|
for (let i = 0; i < totalChunks; i++) {
|
|
const start = i * chunkSize;
|
|
const end = Math.min(start + chunkSize, file.size);
|
|
const chunk = file.slice(start, end);
|
|
|
|
const uploadPromise = fetch(`/api/v2/servers/${serverId}/files/upload/`, {
|
|
method: 'POST',
|
|
body: chunk,
|
|
headers: {
|
|
'Content-Range': `bytes ${start}-${end - 1}/${file.size}`,
|
|
'Content-Length': chunk.size,
|
|
'chunked': true,
|
|
'type': "server_upload",
|
|
'fileSize': file.size,
|
|
'total_chunks': totalChunks,
|
|
'filename': file.name,
|
|
'location': path,
|
|
'fileId': fileId,
|
|
'chunkId': i,
|
|
},
|
|
}).then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === "completed") {
|
|
let caught = false;
|
|
try {
|
|
if (document.getElementById(path).classList.contains("clicked")) {
|
|
var expanded = true;
|
|
}
|
|
} catch {
|
|
var expanded = false;
|
|
}
|
|
|
|
try {
|
|
var par_el = document.getElementById(path + "ul");
|
|
var items = par_el.children;
|
|
} catch (err) {
|
|
console.log(err)
|
|
caught = true;
|
|
var par_el = document.getElementById("files-tree");
|
|
var items = par_el.children;
|
|
}
|
|
let name = file.name;
|
|
console.log(par_el)
|
|
let full_path = path + '/' + name
|
|
let flag = false;
|
|
for (var k = 0; k < items.length; ++k) {
|
|
if ($(items[k]).attr("data-name") == name) {
|
|
flag = true;
|
|
}
|
|
}
|
|
if (!flag) {
|
|
if (caught && expanded == false) {
|
|
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
|
|
} else if (expanded == true) {
|
|
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
|
|
}
|
|
setTreeViewContext();
|
|
}
|
|
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped");
|
|
$(`#upload-progress-bar-${i + 1}`).addClass("bg-success");
|
|
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
|
|
} else if (data.status !== "partial") {
|
|
throw new Error(data.message);
|
|
}
|
|
// Update progress bar
|
|
const progress = (i + 1) / totalChunks * 100;
|
|
updateProgressBar(Math.round(progress), file_num);
|
|
});
|
|
|
|
uploadPromises.push(uploadPromise);
|
|
}
|
|
|
|
try {
|
|
await Promise.all(uploadPromises);
|
|
} catch (error) {
|
|
alert("Error uploading file: " + error.message);
|
|
}
|
|
}
|
|
function updateProgressBar(progress, i) {
|
|
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
|
|
$(`#upload-progress-bar-${i + 1}`).html(progress + '%');
|
|
}
|
|
function uuidv4() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
const r = Math.random() * 16 | 0,
|
|
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
let uploadWaitDialog;
|
|
let doUpload = true;
|
|
|
|
async function uploadFilesE(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
$(function () {
|
|
var uploadHtml = "<div>" +
|
|
'<form id="upload_file" enctype="multipart/form-data">' + "<label class='upload-area' style='width:100%;text-align:center;' for='files'>" +
|
|
"<i class='fa fa-cloud-upload fa-3x'></i>" +
|
|
"<br />" +
|
|
"{{translate('serverFiles', 'clickUpload', data['lang'])}}" +
|
|
"<input style='margin-left: 21%;' id='files' name='files' type='file' multiple='true'>" +
|
|
"</label></form>" +
|
|
"<br />" +
|
|
"<ul style='margin-left:5px !important;' id='fileList'></ul>" +
|
|
"</div><div class='clearfix'></div>";
|
|
bootbox.dialog({
|
|
message: uploadHtml,
|
|
title: "{{ translate('serverFiles', 'uploadTitle', data['lang'])}}" + path,
|
|
buttons: {
|
|
success: {
|
|
label: "{{ translate('serverFiles', 'upload', data['lang']) }}",
|
|
className: "btn-default",
|
|
callback: async function () {
|
|
var height = files.files.length * 50;
|
|
|
|
var waitMessage = '<p class="text-center mb-0">' +
|
|
'<i class="fa fa-spin fa-cog"></i> ' +
|
|
"{{ translate('serverFiles', 'waitUpload', data['lang']) }}" + '<br>' +
|
|
'<strong>' + "{{ translate('serverFiles', 'stayHere', data['lang']) }}" + '</strong>' +
|
|
'</p>' +
|
|
'<div class="progress" id="upload-progress-bar-parent" style="height:' + height + 'px; width:100%; display: block;">' +
|
|
'</div>'
|
|
files = document.getElementById("files");
|
|
uploadWaitDialog = bootbox.dialog({
|
|
message: waitMessage,
|
|
closeButton: false
|
|
});
|
|
|
|
let nFiles = files.files.length;
|
|
const uploadPromises = [];
|
|
|
|
for (let i = 0; i < nFiles; i++) {
|
|
const file = files.files[i];
|
|
const progressHtml = `
|
|
<div style="width: 100%; min-width: 100%;">
|
|
${file.name}:
|
|
<br><div
|
|
id="upload-progress-bar-${i + 1}"
|
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
|
role="progressbar"
|
|
style="width: 100%; height: 10px;"
|
|
aria-valuenow="0"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
></div>
|
|
</div><br>
|
|
`;
|
|
|
|
$('#upload-progress-bar-parent').append(progressHtml);
|
|
|
|
const uploadPromise = uploadFile(file, path, i, (progress) => {
|
|
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
|
|
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
|
|
});
|
|
uploadPromises.push(uploadPromise);
|
|
}
|
|
|
|
try {
|
|
await Promise.all(uploadPromises);
|
|
hideUploadBox();
|
|
} catch (error) {
|
|
alert("Error uploading file: " + error.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function getDirView(event) {
|
|
let path = event.target.parentElement.getAttribute("data-path");
|
|
if (document.getElementById(path).classList.contains('clicked')) {
|
|
return;
|
|
} else {
|
|
getTreeView(path);
|
|
}
|
|
|
|
}
|
|
async function getTreeView(path) {
|
|
const token = getCookie("_xsrf");
|
|
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-XSRFToken': token
|
|
},
|
|
body: JSON.stringify({ "page": "files", "path": path }),
|
|
});
|
|
let responseData = await res.json();
|
|
if (responseData.status === "ok") {
|
|
console.log(responseData);
|
|
process_tree_response(responseData);
|
|
|
|
} else {
|
|
|
|
bootbox.alert({
|
|
title: responseData.status,
|
|
message: responseData.error
|
|
});
|
|
}
|
|
}
|
|
|
|
function process_tree_response(response) {
|
|
let path = response.data.root_path.path;
|
|
let text = ``;
|
|
if (!response.data.root_path.top) {
|
|
text = `<ul class="tree-nested d-block" id="${path}ul">`;
|
|
}
|
|
Object.entries(response.data).forEach(([key, value]) => {
|
|
if (key === "root_path" || key === "db_stats") {
|
|
//continue is not valid in for each. Return acts as a continue.
|
|
return;
|
|
}
|
|
let checked = ""
|
|
let dpath = value.path;
|
|
let filename = key;
|
|
if (value.dir) {
|
|
if (value.excluded) {
|
|
checked = "checked"
|
|
}
|
|
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
|
|
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
|
|
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${checked}>
|
|
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
|
|
<i style="color: var(--info);" class="far fa-folder"></i>
|
|
<i style="color: var(--info);" class="far fa-folder-open"></i>
|
|
${filename}
|
|
</span>
|
|
</input></div></li>`
|
|
} else {
|
|
text += `<li
|
|
class="d-block tree-ctx-item tree-file"
|
|
data-path="${dpath}"
|
|
data-name="${filename}"
|
|
onclick="clickOnFile(event)" id="${dpath}li"><input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
|
|
<i class="far fa-file"></i></span></input>${filename}</li>`
|
|
}
|
|
});
|
|
if (!response.data.root_path.top) {
|
|
text += `</ul>`;
|
|
}
|
|
if (response.data.root_path.top) {
|
|
try {
|
|
document.getElementById('main-tree-div').innerHTML += text;
|
|
document.getElementById('main-tree').parentElement.classList.add("clicked");
|
|
} catch {
|
|
document.getElementById('files-tree').innerHTML = text;
|
|
}
|
|
} else {
|
|
try {
|
|
document.getElementById(path + "span").classList.add('tree-caret-down');
|
|
document.getElementById(path).innerHTML += text;
|
|
document.getElementById(path).classList.add("clicked");
|
|
} catch {
|
|
console.log("Bad")
|
|
}
|
|
|
|
var toggler = document.getElementById(path + "span");
|
|
|
|
if (toggler.classList.contains('files-tree-title')) {
|
|
document.getElementById(path + "span").addEventListener("click", function caretListener() {
|
|
document.getElementById(path + "ul").classList.toggle("d-block");
|
|
document.getElementById(path + "span").classList.toggle("tree-caret-down");
|
|
});
|
|
}
|
|
}
|
|
setTimeout(function () { setTreeViewContext() }, 1000);
|
|
}
|
|
function getToggleMain(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
document.getElementById("files-tree").classList.toggle("d-block");
|
|
document.getElementById(path + "span").classList.toggle("tree-caret-down");
|
|
document.getElementById(path + "span").classList.toggle("tree-caret");
|
|
}
|
|
|
|
function setTreeViewContext() {
|
|
var treeItems = Array.from(document.getElementsByClassName('tree-ctx-item'));
|
|
|
|
treeItems.forEach(item => {
|
|
if ([
|
|
'iPad Simulator',
|
|
'iPhone Simulator',
|
|
'iPod Simulator',
|
|
'iPad',
|
|
'iPhone',
|
|
'iPod'
|
|
].includes(navigator.platform)
|
|
// iPad on iOS 13 detection
|
|
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)) {
|
|
item.addEventListener("touchstart", touchstart, false);
|
|
item.addEventListener("touchend", touchend, false);
|
|
}
|
|
item.addEventListener('contextmenu', function contextListener(event) {
|
|
event.preventDefault();
|
|
var ctxmenuPath = event.target.getAttribute('data-path');
|
|
var ctxmenuName = event.target.getAttribute('data-name');
|
|
if (!ctxmenuPath) {
|
|
console.log({ 'event.target': event.target, ctxmenuPath });
|
|
return;
|
|
}
|
|
$('#renameItem').show();
|
|
|
|
var isDir = event.target.classList.contains('files-tree-title');
|
|
$('#createFile').toggle(isDir);
|
|
$('#createDir').toggle(isDir);
|
|
$('#deleteDir').toggle(isDir);
|
|
$('#upload').toggle(isDir);
|
|
|
|
document.getElementById('context-title').innerHTML = ctxmenuName;
|
|
|
|
var isFile = event.target.classList.contains('tree-file');
|
|
$('#deleteFile').toggle(isFile);
|
|
$('#downloadFile').toggle(isFile);
|
|
|
|
if (event.target.classList.contains('root-dir')) {
|
|
$('#createFile').show();
|
|
$('#createDir').show();
|
|
$('#renameItem').hide();
|
|
$('#deleteDir').hide();
|
|
$('#deleteFile').hide();
|
|
$('#downloadFile').hide();
|
|
$('#upload').show();
|
|
}
|
|
if (event.target.textContent.endsWith('.zip')) {
|
|
$('#unzip').show();
|
|
} else {
|
|
$('#unzip').hide();
|
|
}
|
|
|
|
var clientX = event.clientX;
|
|
var clientY = event.clientY;
|
|
|
|
document.getElementById('files-tree-nav-content')
|
|
.setAttribute('data-path', ctxmenuPath);
|
|
document.getElementById('files-tree-nav-content')
|
|
.setAttribute('data-name', ctxmenuName);
|
|
document.getElementById("files-tree-nav").style.display = "flex";
|
|
document.getElementById("files-tree-nav").style.position = "fixed";
|
|
domRect = document.getElementById("files-tree-nav").getBoundingClientRect();
|
|
sum = (clientY + domRect['height']) - window.innerHeight
|
|
if (domRect['height'] + clientY > window.innerHeight) {
|
|
clientY = clientY - sum
|
|
}
|
|
document.getElementById("files-tree-nav").style.top = clientY + 'px';
|
|
document.getElementById("files-tree-nav").style.left = clientX + 'px';
|
|
})
|
|
})
|
|
}
|
|
|
|
document.addEventListener('click', function (e) {
|
|
let inside = (e.target.closest('#files-tree-nav'));
|
|
let contextMenu = document.getElementById('files-tree-nav');
|
|
if (!inside) {
|
|
contextMenu.setAttribute('style', 'display:none');
|
|
} else {
|
|
contextMenu.setAttribute('style', 'display:none');
|
|
}
|
|
});
|
|
|
|
if (webSocket) {
|
|
webSocket.on('close_upload_box', function (close_upload_box) {
|
|
hideUploadBox();
|
|
});
|
|
}
|
|
function hideUploadBox() {
|
|
if (!uploadWaitDialog) return;
|
|
uploadWaitDialog.modal('hide');
|
|
}
|
|
|
|
function createFileE(event) {
|
|
bootbox.prompt("{% raw translate('serverFiles', 'createFileQuestion', data['lang']) %}", function (result) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
name = event.target.parentElement.getAttribute('data-name');
|
|
if (!result) return;
|
|
|
|
createFile(path, result, function () {
|
|
getTreeView()
|
|
document.getElementById('files-tree-nav').style.display = 'none';
|
|
});
|
|
})
|
|
}
|
|
|
|
function createDirE(event) {
|
|
bootbox.prompt("{% raw translate('serverFiles', 'createDirQuestion', data['lang']) %}", function (result) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
name = event.target.parentElement.getAttribute('data-name');
|
|
if (!result) return;
|
|
|
|
createDir(path, result, function () {
|
|
getTreeView()
|
|
document.getElementById('files-tree-nav').style.display = 'none';
|
|
});
|
|
})
|
|
}
|
|
function downloadFileE(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
name = event.target.parentElement.getAttribute('data-name');
|
|
encoded_path = encodeURIComponent(path)
|
|
encoded_name = encodeURIComponent(name)
|
|
window.location.href = `/panel/download_file?id=${serverId}&path=${encoded_path}&name=${encoded_name}`;
|
|
}
|
|
|
|
function renameItemE(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
name = event.target.parentElement.getAttribute('data-name');
|
|
bootbox.prompt({
|
|
title: "{% raw translate('serverFiles', 'renameItemQuestion', data['lang']) %}",
|
|
value: name,
|
|
callback: function (result) {
|
|
if (!result) return;
|
|
renameItem(path, result, function () {
|
|
getTreeView()
|
|
document.getElementById('files-tree-nav').style.display = 'none';
|
|
});
|
|
}
|
|
});
|
|
}
|
|
function unzipFilesE(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
console.log(path)
|
|
unZip(path)
|
|
}
|
|
|
|
function deleteFileE(event) {
|
|
path = event.target.parentElement.getAttribute('data-path');
|
|
name = event.target.parentElement.getAttribute('data-name');
|
|
bootbox.confirm({
|
|
size: "",
|
|
title: "{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
|
|
closeButton: false,
|
|
message: "{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
|
|
buttons: {
|
|
confirm: {
|
|
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
|
|
className: 'btn-danger'
|
|
},
|
|
cancel: {
|
|
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
|
|
className: 'btn-link'
|
|
}
|
|
},
|
|
callback: function (result) {
|
|
if (!result) return;
|
|
deleteItem(path);
|
|
}
|
|
});
|
|
}
|
|
|
|
getTreeView($('#root_dir').data('path'));
|
|
setTreeViewContext();
|
|
|
|
function setKeyboard(target) {
|
|
var handlerName = target.getAttribute('data-handler-name');
|
|
if (handlerName == 'null') handlerName = null;
|
|
editor.setKeyboardHandler(handlerName, () => {
|
|
if (handlerName == 'ace/keyboard/vim') {
|
|
require("ace/keyboard/vim").Vim.defineEx('write', 'w', function () {
|
|
save();
|
|
});
|
|
}
|
|
});
|
|
|
|
var nodes = target.parentNode.querySelectorAll("[data-handler-name]");
|
|
nodes.forEach(node => {
|
|
node.classList.remove('btn-primary');
|
|
node.classList.add('btn-secondary');
|
|
})
|
|
|
|
target.classList.remove('btn-secondary');
|
|
target.classList.add('btn-primary');
|
|
}
|
|
|
|
|
|
|
|
</script>
|
|
|
|
{% end %} |