feat: enhance volume management with support for multiple volumes and improved API endpoints

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Jakub Dorfman 2026-04-28 23:43:17 +02:00
parent 9b0c3aa190
commit e7c0b56473
3 changed files with 133 additions and 41 deletions

View file

@ -48,6 +48,16 @@ def parse_env_blob(blob):
return out
def get_app_volumes(name):
result = run_panelctl(["inspect-volumes", name])
volumes = {}
if result["ok"]:
for line in result["stdout"].splitlines():
if "|" in line:
vname, vpath = line.split("|", 1)
volumes[vname.strip()] = vpath.strip()
return volumes
def read_app_info(name):
if not is_safe_name(name):
return None, {"ok": False, "error": "invalid app name"}
@ -311,6 +321,16 @@ class Handler(BaseHTTPRequestHandler):
self._json(200, {"ok": True, "app": parse_env_blob(result["stdout"])})
return
# /apps/<name>/volumes
if len(parts) == 3 and parts[0] == "apps" and parts[2] == "volumes":
name = parts[1]
if not is_safe_name(name):
self._json(400, {"ok": False, "error": "invalid app name"})
return
volumes = get_app_volumes(name)
self._json(200, {"ok": True, "name": name, "volumes": volumes})
return
# /apps/<name>/volume/files
if len(parts) == 4 and parts[0] == "apps" and parts[2] == "volume" and parts[3] == "files":
name = parts[1]
@ -319,8 +339,14 @@ class Handler(BaseHTTPRequestHandler):
self._json(404, err)
return
volumes = get_app_volumes(name)
vol_key = query.get("vol", ["default"])[0]
if vol_key not in volumes:
self._json(400, {"ok": False, "error": "invalid volume specified"})
return
subpath = query.get("path", [""])[0].strip("/")
data_dir = os.path.join(app["APP_VOLUME_DIR"], "data")
data_dir = volumes[vol_key]
target_dir = os.path.abspath(os.path.join(data_dir, subpath))
# Ensure traversal didn't escape data_dir
@ -364,12 +390,18 @@ class Handler(BaseHTTPRequestHandler):
self._json(404, err)
return
volumes = get_app_volumes(name)
vol_key = query.get("vol", ["default"])[0]
if vol_key not in volumes:
self._json(400, {"ok": False, "error": "invalid volume specified"})
return
subpath = query.get("path", [""])[0].strip("/")
if not subpath:
self._json(400, {"ok": False, "error": "path parameter required"})
return
data_dir = os.path.join(app["APP_VOLUME_DIR"], "data")
data_dir = volumes[vol_key]
target_file = os.path.abspath(os.path.join(data_dir, subpath))
if not target_file.startswith(os.path.abspath(data_dir)):
@ -406,12 +438,18 @@ class Handler(BaseHTTPRequestHandler):
self._json(404, err)
return
volumes = get_app_volumes(name)
vol_key = query.get("vol", ["default"])[0]
if vol_key not in volumes:
self._json(400, {"ok": False, "error": "invalid volume specified"})
return
subpath = query.get("path", [""])[0].strip("/")
if not subpath:
self._json(400, {"ok": False, "error": "path parameter required"})
return
data_dir = os.path.join(app["APP_VOLUME_DIR"], "data")
data_dir = volumes[vol_key]
target_file = os.path.abspath(os.path.join(data_dir, subpath))
if not target_file.startswith(os.path.abspath(data_dir)):
self._json(403, {"ok": False, "error": "path traversal denied"})
@ -446,12 +484,18 @@ class Handler(BaseHTTPRequestHandler):
self._json(404, err)
return
volumes = get_app_volumes(name)
vol_key = query.get("vol", ["default"])[0]
if vol_key not in volumes:
self._json(400, {"ok": False, "error": "invalid volume specified"})
return
subpath = query.get("path", [""])[0].strip("/")
if not subpath:
self._json(400, {"ok": False, "error": "path parameter required"})
return
data_dir = os.path.join(app["APP_VOLUME_DIR"], "data")
data_dir = volumes[vol_key]
target_file = os.path.abspath(os.path.join(data_dir, subpath))
if not target_file.startswith(os.path.abspath(data_dir)):