diff --git a/api_sender.html b/api_sender.html
index bc6255c..8a02693 100644
--- a/api_sender.html
+++ b/api_sender.html
@@ -2,17 +2,76 @@
- API Interaction Form
+ File Upload and API Interaction Form
+
-Generic Calls
+File Upload Form
+
+
+
+
+API Interaction Form
Update Property
Send Command
Set Subdomain
-
diff --git a/app.py b/app.py
index 102ea8d..1c97743 100644
--- a/app.py
+++ b/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
diff --git a/file_manager.py b/file_manager.py
index 71931aa..f31af0c 100644
--- a/file_manager.py
+++ b/file_manager.py
@@ -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:])
diff --git a/firebase_manager.py b/firebase_manager.py
index a97a3c6..6db0a6d 100644
--- a/firebase_manager.py
+++ b/firebase_manager.py
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 6d13e35..01721e4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,5 @@ mcipc~=2.4.2
firebase-admin~=6.5.0
protobuf~=4.25.3
typing_extensions~=4.9.0
-APScheduler~=3.10.4
\ No newline at end of file
+APScheduler~=3.10.4
+Werkzeug~=3.0.3
\ No newline at end of file
diff --git a/unit_test.py b/unit_test.py
index 35edac8..bc91385 100644
--- a/unit_test.py
+++ b/unit_test.py
@@ -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())
-
-
-