Add file editing functionality to dashboard

Implement API endpoints for retrieving and saving file content, allowing users to edit supported file types directly from the dashboard. Introduce a modal editor interface with syntax highlighting for various file formats. Update the HTML template to include the editor overlay and associated JavaScript for handling file operations, enhancing user experience and interactivity in managing portal files.
This commit is contained in:
nearxos
2026-02-22 16:29:14 +02:00
parent 16c796b8af
commit fd56ed4049
2 changed files with 163 additions and 1 deletions

View File

@@ -1063,6 +1063,96 @@ def api_portal_files_upload():
return jsonify({"ok": False, "error": str(e)}), 500
# Text extensions allowed for editor (others are read-only or binary)
_PORTAL_EDITABLE_EXTENSIONS = frozenset(
".sh .bash .conf .cfg .ini .desktop .json .py .yml .yaml .md .txt .plymouth .script".split()
)
def _portal_language_for_path(name):
"""Return Ace/editor language mode from filename."""
n = (name or "").lower()
if n.endswith(".sh") or n.endswith(".bash"):
return "sh"
if n.endswith(".json"):
return "json"
if n.endswith(".py"):
return "python"
if n.endswith(".yml") or n.endswith(".yaml"):
return "yaml"
if n.endswith(".md"):
return "markdown"
if n.endswith(".conf") or n.endswith(".cfg") or n.endswith(".ini") or n.endswith(".desktop") or n.endswith(".plymouth"):
return "ini"
if n.endswith(".script"):
return "sh"
return "plain_text"
@app.route("/api/portal-files/content", methods=["GET"])
@require_admin
def api_portal_file_content_get():
"""Return file content as text for the editor. path= query param. Rejects binary."""
path_arg = (request.args.get("path") or "").strip()
if not path_arg or ".." in path_arg or "\\" in path_arg:
return jsonify({"ok": False, "error": "invalid path"}), 400
path = (PORTAL_FILES_DIR / path_arg).resolve()
try:
path.relative_to(PORTAL_FILES_DIR.resolve())
except ValueError:
return jsonify({"ok": False, "error": "invalid path"}), 400
if not path.is_file():
return jsonify({"ok": False, "error": "not found"}), 404
ext = path.suffix.lower() if path.suffix else ""
if ext and ext not in _PORTAL_EDITABLE_EXTENSIONS:
return jsonify({"ok": False, "error": "binary or unsupported file type for editing"}), 400
try:
raw = path.read_bytes()
text = raw.decode("utf-8")
except UnicodeDecodeError:
return jsonify({"ok": False, "error": "binary or unsupported encoding"}), 400
except OSError as e:
return jsonify({"ok": False, "error": str(e)}), 500
return jsonify({
"ok": True,
"path": path_arg,
"content": text,
"language": _portal_language_for_path(path.name),
})
@app.route("/api/portal-files/content", methods=["PUT"])
@require_admin
def api_portal_file_content_put():
"""Write file content from editor. JSON body: { \"path\": \"...\", \"content\": \"...\" }."""
data = request.get_json(silent=True) or {}
path_arg = (data.get("path") or "").strip()
content = data.get("content")
if not path_arg or ".." in path_arg or "\\" in path_arg:
return jsonify({"ok": False, "error": "invalid path"}), 400
if content is None:
return jsonify({"ok": False, "error": "content required"}), 400
path = (PORTAL_FILES_DIR / path_arg).resolve()
try:
path.relative_to(PORTAL_FILES_DIR.resolve())
except ValueError:
return jsonify({"ok": False, "error": "invalid path"}), 400
if path.is_dir():
return jsonify({"ok": False, "error": "cannot edit a folder"}), 400
ext = path.suffix.lower() if path.suffix else ""
if path.exists() and ext and ext not in _PORTAL_EDITABLE_EXTENSIONS:
return jsonify({"ok": False, "error": "unsupported file type for editing"}), 400
if not isinstance(content, str):
content = str(content)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
admin_log("portal_edit", path_arg)
return jsonify({"ok": True, "path": path_arg})
except OSError as e:
return jsonify({"ok": False, "error": str(e)}), 500
@app.route("/api/portal-files/<path:name>", methods=["DELETE"])
@require_admin
def api_portal_file_delete(name):