import os
import logging
import json
import html
from jsonschema import validate
from jsonschema.exceptions import ValidationError
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import WebSocketManager, Controller
from app.classes.shared.file_helpers import FileHelpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
files_get_schema = {
"type": "object",
"properties": {
"page": {"type": "string", "minLength": 1},
"path": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_patch_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"contents": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_unzip_schema = {
"type": "object",
"properties": {
"folder": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_create_schema = {
"type": "object",
"properties": {
"parent": {"type": "string"},
"name": {"type": "string"},
"directory": {"type": "boolean"},
},
"additionalProperties": False,
"minProperties": 1,
}
files_rename_schema = {
"type": "object",
"properties": {
"path": {"type": "string"},
"new_name": {"type": "string"},
},
"additionalProperties": False,
"minProperties": 1,
}
file_delete_schema = {
"type": "object",
"properties": {
"filename": {"type": "string", "minLength": 5},
},
"additionalProperties": False,
"minProperties": 1,
}
class ApiServersServerFilesIndexHandler(BaseApiHandler):
def post(self, server_id: str, backup_id=None):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if (
EnumPermissionsServer.FILES not in server_permissions
and EnumPermissionsServer.BACKUP not in server_permissions
):
# if the user doesn't have Files or Backup permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_get_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["path"]):
# TODO: limit some columns for specific permissions?
folder = data["path"]
return_json = {
"root_path": {
"path": folder,
"top": data["path"]
== self.controller.servers.get_server_data_by_id(server_id)["path"],
}
}
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
for item in file_list:
if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item)
else:
unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold
)
for raw_filename in file_list:
filename = html.escape(raw_filename)
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if backup_id:
if str(
dpath
) in self.controller.management.get_excluded_backup_dirs(backup_id):
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": True,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": True,
}
else:
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": False,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": False,
}
else:
if os.path.isdir(rel):
return_json[filename] = {
"path": dpath,
"dir": True,
"excluded": False,
}
else:
return_json[filename] = {
"path": dpath,
"dir": False,
"excluded": False,
}
self.finish_json(200, {"status": "ok", "data": return_json})
else:
try:
with open(data["path"], encoding="utf-8") as file:
file_contents = file.read()
except UnicodeDecodeError as ex:
self.finish_json(
400,
{"status": "error", "error": "DECODE_ERROR", "error_data": str(ex)},
)
self.finish_json(200, {"status": "ok", "data": file_contents})
def delete(self, server_id: str, _backup_id=None):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, file_delete_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["filename"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["filename"]):
proc = FileHelpers.del_dirs(data["filename"])
else:
proc = FileHelpers.del_file(data["filename"])
# disabling pylint because return value could be truthy
# but not a true boolean value
if proc == True: # pylint: disable=singleton-comparison
return self.finish_json(200, {"status": "ok"})
return self.finish_json(500, {"status": "error", "error": str(proc)})
def patch(self, server_id: str, _backup_id):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_patch_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["path"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
file_path = Helpers.get_os_understandable_path(data["path"])
file_contents = data["contents"]
# Open the file in write mode and store the content in file_object
with open(file_path, "w", encoding="utf-8") as file_object:
file_object.write(file_contents)
return self.finish_json(200, {"status": "ok"})
def put(self, server_id: str, _backup_id):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesCreateHandler(BaseApiHandler):
def patch(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_rename_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = data["path"]
new_item_name = data["new_name"]
new_item_path = os.path.join(os.path.split(path)[0], new_item_name)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
) or not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
new_item_path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(new_item_path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": {},
},
)
os.rename(path, new_item_path)
return self.finish_json(200, {"status": "ok"})
def put(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_create_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
path = os.path.join(data["parent"], data["name"])
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_path_exists(os.path.abspath(path)):
return self.finish_json(
400,
{
"status": "error",
"error": "FILE EXISTS",
"error_data": str(e),
},
)
if data["directory"]:
os.mkdir(path)
else:
# Create the file by opening it
with open(path, "w", encoding="utf-8") as file_object:
file_object.close()
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesZipHandler(BaseApiHandler):
def post(self, server_id: str):
auth_data = self.authenticate_user()
if not auth_data:
return
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
try:
data = json.loads(self.request.body)
except json.decoder.JSONDecodeError as e:
return self.finish_json(
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
)
try:
validate(data, files_unzip_schema)
except ValidationError as e:
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_JSON_SCHEMA",
"error_data": str(e),
},
)
folder = data["folder"]
user_id = auth_data[4]["user_id"]
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
folder,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if Helpers.check_file_exists(folder):
folder = self.file_helper.unzip_file(folder, user_id)
else:
if user_id:
return self.finish_json(
400,
{
"status": "error",
"error": "FILE_DOES_NOT_EXIST",
"error_data": str(e),
},
)
return self.finish_json(200, {"status": "ok"})
class ApiServersServerFilesUploadHandler(BaseApiHandler):
async def post(self, server_id: str):
for header, value in self.request.headers.items():
print(f"{header}: {value}")
fileHash = self.request.headers.get("fileHash", 0)
chunkHash = self.request.headers.get("chunk-hash", 0)
file_size = self.request.headers.get("fileSize", None)
self.file_id = self.request.headers.get("fileId")
self.chunked = self.request.headers.get("chunked", True)
self.filename = self.request.headers.get("filename", None)
try:
total_chunks = int(self.request.headers.get("total_chunks", None))
except TypeError:
return self.finish_json(
400, {"status": "error", "data": "INVALID CHUNK COUNT"}
)
self.chunk_index = self.request.headers.get("chunkId")
self.location = self.request.headers.get("location", None)
self.upload_dir = self.location
self.temp_dir = os.path.join(self.controller.project_root, "temp", self.file_id)
if self.chunked and not self.chunk_index:
return self.finish_json(
200, {"status": "ok", "data": {"file-id": self.file_id}}
)
# Create the upload and temp directories if they don't exist
os.makedirs(self.upload_dir, exist_ok=True)
os.makedirs(self.temp_dir, exist_ok=True)
# Read headers and query parameters
content_length = int(self.request.headers.get("Content-Length"))
if content_length <= 0:
return self.finish_json(
400, {"status": "error", "data": {"message": "Invalid content length"}}
)
if not self.filename or self.chunk_index is None or total_chunks is None:
return self.finish_json(
400,
{
"status": "error",
"data": {
"message": "Filename, chunk_index,"
" and total_chunks are required"
},
},
)
# File paths
file_path = os.path.join(self.upload_dir, self.filename)
chunk_path = os.path.join(
self.temp_dir, f"{self.filename}.part{self.chunk_index}"
)
# Save the chunk
with open(chunk_path, "wb") as f:
f.write(self.request.body)
# Check if all chunks are received
received_chunks = [
f
for f in os.listdir(self.temp_dir)
if f.startswith(f"{self.filename}.part")
]
if len(received_chunks) == total_chunks:
with open(file_path, "wb") as outfile:
for i in range(total_chunks):
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
with open(chunk_file, "rb") as infile:
outfile.write(infile.read())
os.remove(chunk_file)
self.write(
json.dumps(
{"status": "completed", "message": "File uploaded successfully"}
)
)
else:
self.write(
json.dumps(
{
"status": "partial",
"message": f"Chunk {self.chunk_index} received",
}
)
)