diff --git a/python/web/src/static/themes/classic/style.css b/python/web/src/static/themes/classic/style.css index 1399a6a7..23b4ab88 100644 --- a/python/web/src/static/themes/classic/style.css +++ b/python/web/src/static/themes/classic/style.css @@ -104,6 +104,15 @@ ul.inline_list { list-style: none; } +summary.dirname { + text-decoration: underline; + font-family: monospace; +} + +summary.filename { + text-decoration: underline; +} + .dropzone, .dropzone * { box-sizing: border-box; } diff --git a/python/web/src/static/themes/modern/style.css b/python/web/src/static/themes/modern/style.css index f7fb707d..361f5bed 100644 --- a/python/web/src/static/themes/modern/style.css +++ b/python/web/src/static/themes/modern/style.css @@ -53,7 +53,7 @@ input[type="radio"] { margin: 0 0.1rem 0 0.75rem; } -div.noscriptmsg { +div.notice { background: var(--danger); border-radius: var(--border-radius); padding: 0.5rem; @@ -717,6 +717,16 @@ section#files p { margin-top: 1rem; } +section#files details.subdir summary.dirname { + text-decoration: underline; + font-family: monospace; + margin: 0.5rem 0; +} + +section#files details.contents summary.filename { + text-decoration: underline; +} + @media (max-width: 900px) { section#files table#images tr th:nth-child(2), section#files table#images tr td:nth-child(2) { diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index d6aa285f..ae4e4dc2 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -209,6 +209,19 @@ +{% if not files|length: %} +
+ {{ _("The images directory is currently empty.") }} +
+{% else %} + +
+{% for subdir, group in formatted_image_files.items() %} + +
+ + {{ subdir }} + @@ -216,19 +229,12 @@ - {% if not files|length: %} - - - - {% endif %} - {% for file in files|sort(attribute='name') %} + {% for file in group|sort(attribute='name') %} {% if file["prop"] %} {% elif file["archive_contents"] %}
{{ _("Size") }} {{ _("Actions") }}
- {{ _("The images directory is currently empty.") }} -
-
- +
+ {{ file["name"] }}
    @@ -244,8 +250,8 @@
-
- +
+ {{ file["name"] }}
    @@ -253,8 +259,8 @@ {% if not member["is_properties_file"] %}
  • {% if member["related_properties_file"] %} -
    - +
    +
    @@ -361,6 +367,12 @@ {% endfor %}
+{% if subdir != "/" %} +
+{% endif %} +{% endfor %} +
+{% endif %}

{{ _("%(disk_space)s MiB disk space remaining on the system", disk_space=env["free_disk_space"]) }}

@@ -396,7 +408,7 @@ -
+
{{ _("The file uploading functionality requires JavaScript.") }}
diff --git a/python/web/src/templates/upload.html b/python/web/src/templates/upload.html index 73a205a2..9a8df9e8 100644 --- a/python/web/src/templates/upload.html +++ b/python/web/src/templates/upload.html @@ -14,6 +14,14 @@ + diff --git a/python/web/src/web.py b/python/web/src/web.py index c0b780a5..f7b09148 100644 --- a/python/web/src/web.py +++ b/python/web/src/web.py @@ -49,6 +49,7 @@ from web_utils import ( map_device_types_and_names, get_device_name, map_image_file_descriptions, + format_image_list, format_drive_properties, get_properties_by_drive_name, auth_active, @@ -223,12 +224,7 @@ def index(): image_files = file_cmd.list_images() config_files = file_cmd.list_config_files() ip_addr, host = sys_cmd.get_ip_and_host() - - extended_image_files = [] - for image in image_files["files"]: - if image["detected_type"] != "UNDEFINED": - image["detected_type_name"] = device_types[image["detected_type"]]["name"] - extended_image_files.append(image) + formatted_image_files = format_image_list(image_files["files"], device_types) attached_images = [] units = 0 @@ -266,7 +262,8 @@ def index(): bridge_configured=sys_cmd.is_bridge_setup(), devices=formatted_devices, attached_images=attached_images, - files=extended_image_files, + formatted_image_files=formatted_image_files, + files=image_files["files"], config_files=config_files, device_types=device_types, scan_depth=server_info["scan_depth"], @@ -317,10 +314,13 @@ def upload_page(): """ Sets up the data structures and kicks off the rendering of the file uploading page """ + image_files = file_cmd.list_images() + formatted_image_files = format_image_list(image_files["files"]) return response( template="upload.html", page_title=_("PiSCSI File Upload"), + formatted_image_files=formatted_image_files, max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024), CFG_DIR=CFG_DIR, FILE_SERVER_DIR=FILE_SERVER_DIR, @@ -990,15 +990,19 @@ def upload_file(): return make_response(auth["msg"], 403) destination = request.form.get("destination") + subdir = request.form.get("subdir") if destination == "disk_images": + safe_path = is_safe_path(Path(subdir)) + if not safe_path["status"]: + return make_response(safe_path["msg"], 403) server_info = piscsi_cmd.get_server_info() - destination_dir = server_info["image_dir"] + destination_dir = Path(server_info["image_dir"]) / subdir elif destination == "shared_files": destination_dir = FILE_SERVER_DIR elif destination == "piscsi_config": destination_dir = CFG_DIR else: - return make_response("Invalid destination", 403) + return make_response("Unknown destination", 403) return upload_with_dropzonejs(destination_dir) diff --git a/python/web/src/web_utils.py b/python/web/src/web_utils.py index c9a42dc4..e007619f 100644 --- a/python/web/src/web_utils.py +++ b/python/web/src/web_utils.py @@ -7,6 +7,7 @@ from grp import getgrall from os import path from pathlib import Path from ua_parser import user_agent_parser +from re import findall from flask import request, make_response from flask_babel import _ @@ -146,6 +147,33 @@ def get_image_description(file_suffix): return file_suffix +def format_image_list(image_files, device_types=None): + """ + Takes a (list) of (dict) image_files and optional (list) device_types + Returns a formatted (dict) with groups of image_files per subdir key + """ + + root_image_files = [] + subdir_image_files = {} + for image in image_files: + if (image["detected_type"] != "UNDEFINED") and device_types: + image["detected_type_name"] = device_types[image["detected_type"]]["name"] + subdir_path = findall("^.*/", image["name"]) + if subdir_path: + subdir = subdir_path[0] + if subdir in subdir_image_files.keys(): + subdir_image_files[f"images/{subdir}"].append(image) + else: + subdir_image_files[f"images/{subdir}"] = [image] + else: + root_image_files.append(image) + + formatted_image_files = dict(sorted(subdir_image_files.items())) + if root_image_files: + formatted_image_files["images/"] = root_image_files + return formatted_image_files + + def format_drive_properties(drive_properties): """ Takes a (dict) with structured drive properties data @@ -256,10 +284,10 @@ def is_safe_path(file_name): Returns True if the path is safe Returns False if the path is either absolute, or tries to traverse the file system """ - if file_name.is_absolute() or ".." in str(file_name): + if file_name.is_absolute() or ".." in str(file_name) or str(file_name)[0] == "~": return { "status": False, - "msg": _("%(file_name)s is not a valid path", file_name=file_name), + "msg": _("No permission to use path '%(file_name)s'", file_name=file_name), } return {"status": True, "msg": ""} diff --git a/python/web/tests/api/test_files.py b/python/web/tests/api/test_files.py index 0bdc3edd..7da66f12 100644 --- a/python/web/tests/api/test_files.py +++ b/python/web/tests/api/test_files.py @@ -254,6 +254,7 @@ def test_upload_file(http_client, delete_file): form_data = { "destination": "disk_images", + "subdir": "", "dzuuid": str(uuid.uuid4()), "dzchunkindex": chunk_number, "dzchunksize": chunk_size,