web UI: fallback no-js web form for file uploads (#1364) (#1560)

This makes it possible to upload files from a vintage browser, for
instance

Note that you have to explicitly turn off javascript in the user agent
to get the fallback form
This commit is contained in:
stinkerton18
2025-11-23 00:54:31 -08:00
committed by GitHub
3 changed files with 114 additions and 31 deletions
+1 -1
View File
@@ -598,7 +598,7 @@
</section>
<section id="upload">
<a href="/upload" target="_blank"><p>{{ _("Upload Files (new tab)") }}</p></a>
<a href="/upload" target="_blank"><p>{{ _("Upload Files (new window)") }}</p></a>
</section>
<noscript>
<style type="text/css">
+69 -30
View File
@@ -11,33 +11,45 @@
<li>{{ _("PiSCSI Config") }} = {{ CFG_DIR }}</li>
</ul>
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
<fieldset>
<legend>{{ _("Destination") }}</legend>
<label for="disk_images" class="hidden">{{ _("Disk Images") }}</label>
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="images_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %}
<option value="{{dir}}">{{ env['image_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['image_root_dir'] }}/</option>
</select>
{% if file_server_dir_exists %}
<label for="shared_files" class="hidden">{{ _("Shared Files") }}</label>
<input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %}
<option value="{{dir}}">{{ env['shared_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['shared_root_dir'] }}/</option>
</select>
{% endif %}
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
</fieldset>
</form>
<style>
#js-upload-form { display: none; }
</style>
<div id="js-upload-form">
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper">
<fieldset>
<legend>{{ _("Destination") }}</legend>
<label for="disk_images" class="hidden">{{ _("Disk Images") }}</label>
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="images_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %}
<option value="{{dir}}">{{ env['image_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['image_root_dir'] }}/</option>
</select>
{% if file_server_dir_exists %}
<label for="shared_files" class="hidden">{{ _("Shared Files") }}</label>
<input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %}
<option value="{{dir}}">{{ env['shared_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['shared_root_dir'] }}/</option>
</select>
{% endif %}
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
</fieldset>
</form>
</div>
<script type="application/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("js-upload-form").style.display = "block";
});
</script>
<script
type="application/javascript"
@@ -82,9 +94,36 @@
</script>
<noscript>
<div class="noscriptmsg">
{{ _("The file uploading functionality requires JavaScript.") }}
</div>
<form id="uploadForm" action="/files/uploadform/" onchange="fileSelect(event)" method="post" enctype="multipart/form-data">
<fieldset>
<legend>{{ _("Destination") }}</legend>
<label for="disk_images" class="hidden">{{ _("Disk Images") }}</label>
<input type="radio" name="destination" id="disk_images" value="disk_images" checked="checked">
<label for="images_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="images_subdir" id="images_subdir">
{% for dir in images_subdirs %}
<option value="{{dir}}">{{ env['image_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['image_root_dir'] }}/</option>
</select>
{% if file_server_dir_exists %}
<label for="shared_files" class="hidden">{{ _("Shared Files") }}</label>
<input type="radio" name="destination" id="shared_files" value="shared_files">
<label for="shared_subdir" class="hidden">{{ _("To directory:") }}</label>
<select name="shared_subdir" id="shared_subdir">
{% for dir in shared_subdirs %}
<option value="{{dir}}">{{ env['shared_root_dir'] }}/{{dir}}</option>
{% endfor %}
<option value="" selected>{{ env['shared_root_dir'] }}/</option>
</select>
{% endif %}
<input type="radio" name="destination" id="piscsi_config" value="piscsi_config">
<label for="piscsi_config">{{ _("PiSCSI Config") }}</label>
</fieldset>
<label for="file">File:</label>
<input type="file" name="file"/>
<input type="submit" value="Upload" />
</form>
</noscript>
{% endblock content %}
+44
View File
@@ -8,6 +8,7 @@ import argparse
from pathlib import Path, PurePath
from functools import wraps
from grp import getgrall
from io import DEFAULT_BUFFER_SIZE
from os import path
import bjoern
@@ -1070,6 +1071,49 @@ def upload_file():
return make_response(_("File upload successful!"), 200)
@APP.route("/files/uploadform/", methods=["POST"])
@login_required
def upload_file_form():
file_object = request.files.get("file")
filename = file_object.filename if file_object else None
destination = request.form.get("destination")
images_subdir = request.form.get("images_subdir")
shared_subdir = request.form.get("shared_subdir")
if destination == "disk_images":
safe_path = is_safe_path(Path(images_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
server_info = piscsi_cmd.get_server_info()
destination_dir = Path(server_info["image_dir"]) / images_subdir
elif destination == "shared_files":
safe_path = is_safe_path(Path(shared_subdir))
if not safe_path["status"]:
return make_response(safe_path["msg"], 403)
destination_dir = Path(FILE_SERVER_DIR) / shared_subdir
elif destination == "piscsi_config":
destination_dir = Path(CFG_DIR)
else:
return make_response(_("Unknown destination"), 403)
if not filename:
flash("No file provided.", "error")
return redirect(url_for("index"))
file_path = path.join(destination_dir, filename)
if path.isfile(file_path):
flash(f"{filename} already exists.", "error")
return redirect(url_for("index"))
binary_new_file = "bx"
with open(file_path, binary_new_file, buffering=DEFAULT_BUFFER_SIZE) as f:
chunk_size = DEFAULT_BUFFER_SIZE
while True:
chunk = file_object.stream.read(chunk_size)
if len(chunk) == 0:
break
f.write(chunk)
return redirect(url_for("index", filename=filename))
@APP.route("/files/create", methods=["POST"])
@login_required
def create_file():