mirror of
https://github.com/hubHarmony/servii-backend.git
synced 2024-11-17 21:40:31 +00:00
[+] Upload route
New upload route for the servii API Now it only allows the user to send jar and zip files as this: - Jar files are considered as plugins and are stored in the plugins folder of the server - zip files are considered as datapacks and are therefore stored in the datapacks folder within the defaults world folder on the server
This commit is contained in:
commit
29aa5a40e3
156
api_sender.html
156
api_sender.html
@ -2,17 +2,76 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>API Interaction Form</title>
|
||||
<title>File Upload and API Interaction Form</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
input[type="file"], input[type="text"], input[type="number"] {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
button {
|
||||
padding: 12px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
.message {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Generic Calls</h2>
|
||||
<h2>File Upload Form</h2>
|
||||
<form id="uploadForm">
|
||||
<input type="file" id="files" accept=".zip,.jar,.txt" multiple required>
|
||||
<button type="submit">Upload File</button>
|
||||
</form>
|
||||
|
||||
<div id="message" class="message"></div>
|
||||
|
||||
<h2>API Interaction Form</h2>
|
||||
<form id="genericForm">
|
||||
Email: <label for="accountEmail"></label><input type="text" id="accountEmail"><br>
|
||||
Port: <label for="accountPort"></label><input type="number" id="accountPort"><br>
|
||||
Name: <label for="serverName"></label><input type="text" id="serverName"><br>
|
||||
Version: <label for="serverVersion"></label><input type="text" id="serverVersion"><br>
|
||||
Framework: <label for="serverFramework"></label><input type="text" id="serverFramework"><br>
|
||||
Email: <input type="text" id="accountEmail"><br>
|
||||
Port: <input type="number" id="accountPort"><br>
|
||||
Name: <input type="text" id="serverName"><br>
|
||||
Version: <input type="text" id="serverVersion"><br>
|
||||
Framework: <input type="text" id="serverFramework"><br>
|
||||
<button type="button" class="actionButton" data-action="AccountCreate">Create Account</button>
|
||||
<button type="button" class="actionButton" data-action="AccountDelete">Delete Account</button>
|
||||
<button type="button" class="actionButton" data-action="ServerCreate">Create Server</button>
|
||||
@ -22,34 +81,84 @@
|
||||
<button type="button" class="actionButton" data-action="FetchServers">Fetch Servers</button>
|
||||
<button type="button" class="actionButton" data-action="FetchLogs">Fetch Logs</button>
|
||||
<button type="button" class="actionButton" data-action="FetchPlayersStatus">Fetch Players Status</button>
|
||||
|
||||
</form>
|
||||
|
||||
<h2>Update Property</h2>
|
||||
<form id="updatePropertyForm">
|
||||
Property: <label for="update_property"></label><input type="text" id="update_property"><br>
|
||||
Value: <label for="update_value"></label><input type="text" id="update_value"><br>
|
||||
Property: <input type="text" id="update_property"><br>
|
||||
Value: <input type="text" id="update_value"><br>
|
||||
<button type="button" class="actionButton" data-action="UpdateProperties">Update Property</button>
|
||||
</form>
|
||||
|
||||
<h2>Send Command</h2>
|
||||
<form id="sendCommandForm">
|
||||
Command: <label for="command"></label><input type="text" id="command"><br>
|
||||
Command: <input type="text" id="command"><br>
|
||||
<button type="button" class="actionButton" data-action="Command">Send command</button>
|
||||
</form>
|
||||
|
||||
<h2>Set Subdomain</h2>
|
||||
<form id="sendCommandForm">
|
||||
Command: <label for="subdomain"></label><input type="text" id="subdomain"><br>
|
||||
<button type="button" class="actionButton" data-action="SetSubdomain">Send command</button>
|
||||
<form id="setSubdomainForm">
|
||||
Subdomain: <input type="text" id="subdomain"><br>
|
||||
<button type="button" class="actionButton" data-action="SetSubdomain">Set Subdomain</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const uploadForm = document.getElementById('uploadForm');
|
||||
const genericForm = document.getElementById('genericForm');
|
||||
const updatePropertyForm = document.getElementById('updatePropertyForm');
|
||||
const sendCommandForm = document.getElementById('sendCommandForm');
|
||||
const setSubdomainForm = document.getElementById('setSubdomainForm');
|
||||
const messageDiv = document.getElementById('message');
|
||||
|
||||
// File Upload functionality
|
||||
uploadForm.addEventListener('submit', async event => {
|
||||
event.preventDefault();
|
||||
const fileInput = document.getElementById('files');
|
||||
const filesLength = fileInput.files.length;
|
||||
|
||||
if (filesLength === 0) {
|
||||
showMessage('Please select at least one file.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', 'local');
|
||||
formData.append('token', 'MpkbDMOO8PQddQgB5VgBQdTMWF53');
|
||||
for (let i = 0; i < filesLength; i++) {
|
||||
formData.append(`${i}`, fileInput.files[i]);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/Upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(errorMessage, 'text/html');
|
||||
const title = doc.querySelector('title')?.textContent || 'Unknown Error';
|
||||
const description = doc.querySelector('p')?.textContent || 'Unable to determine error description.';
|
||||
|
||||
showMessage(`${title}: ${description}`, 'error');
|
||||
} else {
|
||||
const result = await response.json();
|
||||
showMessage(`File uploaded successfully. Filename: ${result.filename}`, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showMessage('An error occurred. Please try again.', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// API Interaction functionality
|
||||
const buttons = document.querySelectorAll('.actionButton');
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
const action = button.dataset.action;
|
||||
const token = "MpkbDMOO8PQddQgB5VgBQdTMWF53";
|
||||
const framework = document.getElementById('serverFramework').value;
|
||||
@ -63,6 +172,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const command = document.getElementById('command').value;
|
||||
const props = [[prop, value], ["max-players", "666"]];
|
||||
let data = {};
|
||||
|
||||
switch(action) {
|
||||
case 'FetchServers':
|
||||
data = {token};
|
||||
@ -98,13 +208,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
data = {port, name, command, token};
|
||||
break;
|
||||
case 'SetSubdomain':
|
||||
data = {token, subdomain}
|
||||
data = {token, subdomain};
|
||||
break;
|
||||
}
|
||||
|
||||
sendRequest(action, data)
|
||||
.then(response => response.text())
|
||||
.then(data => alert(`Response: ${data}`))
|
||||
.catch(error => console.error('Error:', error));
|
||||
.then(response => response.text())
|
||||
.then(data => alert(`Response: ${data}`))
|
||||
.catch(error => console.error('Error:', error));
|
||||
});
|
||||
});
|
||||
|
||||
@ -117,6 +228,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
|
||||
function showMessage(message, type) {
|
||||
messageDiv.textContent = message;
|
||||
messageDiv.className = `message ${type}`;
|
||||
setTimeout(() => {
|
||||
messageDiv.textContent = '';
|
||||
messageDiv.className ='message';
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
66
app.py
66
app.py
@ -1,16 +1,23 @@
|
||||
import atexit
|
||||
import http
|
||||
import inspect
|
||||
import os
|
||||
from mailbox import FormatError
|
||||
from typing import Union
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from firebase_admin.auth import UserNotFoundError, UserRecord
|
||||
from flask import (Blueprint, Flask, Response, jsonify, request)
|
||||
from flask_cors import CORS
|
||||
from werkzeug.datastructures import ImmutableMultiDict, FileStorage
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import file_manager
|
||||
import firebase_manager
|
||||
import generic_executor
|
||||
import atexit
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['MAX_CONTENT_LENGTH'] = 16 * 1000 * 1000 * 1000
|
||||
cors = CORS(app, origins="*")
|
||||
apiBP = Blueprint('apiBP', 'BPapi')
|
||||
|
||||
@ -28,7 +35,7 @@ def generic_response_maker(status_code: http.HTTPStatus, _message: str = None) -
|
||||
case http.HTTPStatus.ACCEPTED:
|
||||
message = jsonify({'message': 'Action successful.'})
|
||||
case http.HTTPStatus.BAD_REQUEST:
|
||||
message = jsonify({'message': 'Bad Request, the property you tried to modify is not valid.'})
|
||||
message = jsonify({'message': 'Bad Request.'})
|
||||
case http.HTTPStatus.NOT_FOUND:
|
||||
message = jsonify({'message': 'Server not found.'})
|
||||
case http.HTTPStatus.UNSUPPORTED_MEDIA_TYPE:
|
||||
@ -118,6 +125,59 @@ def dynamic_route_handler(path):
|
||||
return generic_response_maker(http.HTTPStatus.BAD_REQUEST, str(e))
|
||||
|
||||
|
||||
# [!] This route is specific and has to remain out of the dynamic route handler.
|
||||
@apiBP.route('/Upload', methods=['POST'])
|
||||
def upload():
|
||||
form = request.form
|
||||
token: str or None = form.get('token')
|
||||
name: str or None = form.get('name')
|
||||
files: ImmutableMultiDict[str, FileStorage] = request.files
|
||||
|
||||
try:
|
||||
if not form:
|
||||
raise FormatError(0)
|
||||
if not files:
|
||||
raise FormatError(1)
|
||||
if not token:
|
||||
raise KeyError('token')
|
||||
if not name:
|
||||
raise KeyError('name')
|
||||
|
||||
data: dict = {'token': token}
|
||||
user: UserRecord = authenticate_request(data)
|
||||
user_id: str = user.uid
|
||||
|
||||
for _, file in files.items():
|
||||
filename = file.filename
|
||||
internal_path = file_manager.get_path_from_extension(filename)
|
||||
if internal_path is None:
|
||||
continue
|
||||
filename = secure_filename(filename)
|
||||
file.save(os.path.join(f"users/{user_id}/{name}/{internal_path}", filename))
|
||||
|
||||
except FormatError as e:
|
||||
match e:
|
||||
case 0:
|
||||
return generic_response_maker(http.HTTPStatus.BAD_REQUEST, "No FormData found in the payload.")
|
||||
case 1:
|
||||
return generic_response_maker(http.HTTPStatus.NOT_ACCEPTABLE, "No file(s) were uploaded.")
|
||||
|
||||
except FileNotFoundError:
|
||||
return generic_response_maker(http.HTTPStatus.CONFLICT, "Please launch the server at least once.")
|
||||
|
||||
except KeyError as e:
|
||||
return f"Missing parameter {e} in FormData.", http.HTTPStatus.BAD_REQUEST
|
||||
|
||||
except UserNotFoundError as e:
|
||||
return str(e), http.HTTPStatus.BAD_REQUEST
|
||||
|
||||
except Exception as e:
|
||||
file_manager.log_error(type(e).__name__, str(e))
|
||||
return generic_response_maker(http.HTTPStatus.BAD_REQUEST, f"{type(e).__name__}, {str(e)}")
|
||||
|
||||
return generic_response_maker(http.HTTPStatus.OK, "Successfully uploaded files !")
|
||||
|
||||
|
||||
def exit_safety() -> None:
|
||||
firebase_manager.set_servers_not_running()
|
||||
return
|
||||
|
@ -86,6 +86,22 @@ def log_action(user_id: str, name: str, action: str, details: str = None):
|
||||
log_error(type(e).__name__, str(e) + " error trying to access history file on not existing server.")
|
||||
|
||||
|
||||
def get_path_from_extension(filename) -> str or None:
|
||||
allowed_extensions = {
|
||||
'zip': "world/datapacks",
|
||||
'jar': "plugins"
|
||||
}
|
||||
filename_lower = filename.strip().lower()
|
||||
if '.' not in filename_lower:
|
||||
return None
|
||||
|
||||
file_extension = filename_lower.split('.')[-1]
|
||||
if file_extension in allowed_extensions:
|
||||
return allowed_extensions[file_extension]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def kebab_to_camel_case(s: str) -> str:
|
||||
parts = s.split('-')
|
||||
return parts[0] + ''.join(part.title() for part in parts[1:])
|
||||
|
@ -6,6 +6,7 @@ import firebase_admin
|
||||
import jwt
|
||||
from firebase_admin import auth, credentials, firestore
|
||||
from google.api_core.exceptions import Aborted, DataLoss, NotFound, OutOfRange, PermissionDenied, ResourceExhausted
|
||||
from google.cloud.firestore_v1 import FieldFilter
|
||||
|
||||
import file_manager
|
||||
from generic_executor import mc_manager
|
||||
@ -51,7 +52,7 @@ def user_field_exists(user_id: str) -> bool:
|
||||
|
||||
def server_name_taken(user_id: str, server_name: str) -> bool:
|
||||
servers = firestore_database.collection('users').document(user_id).collection('servers')
|
||||
query = servers.where(field_path='name', op_string='==', value=server_name)
|
||||
query = servers.where(filter=FieldFilter(field_path='name', op_string='==', value=server_name))
|
||||
for _ in query.stream():
|
||||
return True
|
||||
return False
|
||||
|
@ -11,3 +11,4 @@ firebase-admin~=6.5.0
|
||||
protobuf~=4.25.3
|
||||
typing_extensions~=4.9.0
|
||||
APScheduler~=3.10.4
|
||||
Werkzeug~=3.0.3
|
@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
|
||||
import firebase_manager
|
||||
|
||||
|
||||
@ -41,6 +42,3 @@ if __name__ == '__main__':
|
||||
#file_manager.log_action("gqZN3eCHF3V2er3Py3rlgk8u2t83", "test", "DeleteServer")
|
||||
#firebase_manager.set_servers_not_running()
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user