Merge pull request #250 from akuker/fix_uploads

Fix the file upload functionality
This commit is contained in:
Eric Helgeson 2021-09-24 18:35:18 -05:00 committed by GitHub
commit 01443ec653
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 183 additions and 78 deletions

View File

@ -1,5 +1,4 @@
import os import os
import subprocess
import logging import logging
from ractl_cmds import ( from ractl_cmds import (
@ -63,17 +62,23 @@ def delete_file(file_name):
def unzip_file(file_name): def unzip_file(file_name):
import zipfile from subprocess import run
with zipfile.ZipFile(base_dir + file_name, "r") as zip_ref: unzip_proc = run(
zip_ref.extractall(base_dir) ["unzip", "-d", base_dir, "-o", "-j", base_dir + file_name], capture_output=True
return True )
if unzip_proc.returncode != 0:
logging.warning(f"Unzipping failed: {unzip_proc}")
return {"status": False, "msg": unzip_proc}
return {"status": True, "msg": f"{file_name} unzipped"}
def download_file_to_iso(scsi_id, url): def download_file_to_iso(scsi_id, url):
import urllib.request import urllib.request
import urllib.error as error import urllib.error as error
import time import time
from subprocess import run
file_name = url.split("/")[-1] file_name = url.split("/")[-1]
tmp_ts = int(time.time()) tmp_ts = int(time.time())
@ -91,7 +96,7 @@ def download_file_to_iso(scsi_id, url):
return {"status": False, "msg": "Error loading the URL"} return {"status": False, "msg": "Error loading the URL"}
# iso_filename = make_cd(tmp_full_path, None, None) # not working yet # iso_filename = make_cd(tmp_full_path, None, None) # not working yet
iso_proc = subprocess.run( iso_proc = run(
["genisoimage", "-hfs", "-o", iso_filename, tmp_full_path], capture_output=True ["genisoimage", "-hfs", "-o", iso_filename, tmp_full_path], capture_output=True
) )
if iso_proc.returncode != 0: if iso_proc.returncode != 0:

View File

@ -39,3 +39,9 @@ def is_bridge_setup():
if "rascsi_bridge" in output: if "rascsi_bridge" in output:
return True return True
return False return False
def disk_space():
from shutil import disk_usage
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}

View File

@ -11,3 +11,4 @@ waitress==1.4.4
zope.event==4.5.0 zope.event==4.5.0
zope.interface==5.1.2 zope.interface==5.1.2
protobuf==3.17.3 protobuf==3.17.3
pydrop==0.0.6

View File

@ -1,14 +1,14 @@
<html> <html>
<head> <head>
<title>RaSCSI-web is Starting</title> <title>RaSCSI-Web is Starting</title>
<meta http-equiv="refresh" content="2"> <meta http-equiv="refresh" content="2">
</head> </head>
</html> </html>
<body> <body>
<center> <center>
<h1>RaSCSI Web is starting....</h1> <h1>RaSCSI-Web is Starting....</h1>
<h2>This page will automatically refresh.</h2> <h2>This page will automatically refresh.</h2>
<p>First boot and upgrades can take a second to resolve dependancies.</p> <p>First boot and upgrades can take a second while resolving dependencies.</p>
<p>If you're seeing this page for over a minute please check the logs at <tt>sudo journalctl -f</tt></p> <p>If you're seeing this page for over a minute please check the logs at <tt>sudo journalctl -f</tt></p>
</center> </center>
</body> </body>

View File

@ -4,7 +4,7 @@ base_dir = getenv("BASE_DIR", "/home/pi/images/")
home_dir = getcwd() home_dir = getcwd()
DEFAULT_CONFIG = "default.json" DEFAULT_CONFIG = "default.json"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", 1024 * 1024 * 1024 * 2) # 2gb MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", 1024 * 1024 * 1024 * 4) # 4gb
HARDDRIVE_FILE_SUFFIX = ("hda", "hdn", "hdi", "nhd", "hdf", "hds") HARDDRIVE_FILE_SUFFIX = ("hda", "hdn", "hdi", "nhd", "hdf", "hds")
CDROM_FILE_SUFFIX = ("iso", "cdr", "toast", "img") CDROM_FILE_SUFFIX = ("iso", "cdr", "toast", "img")

View File

@ -47,3 +47,61 @@ td.inactive {
text-align:center; text-align:center;
background-color:tan; background-color:tan;
} }
.dropzone, .dropzone * {
box-sizing: border-box;
}
.dropzone {
position: relative;
}
.dropzone .dz-preview {
position: relative;
display: inline-block;
width: 120px;
margin: .5em;
}
.dropzone .dz-preview .dz-progress {
display: block;
height: 15px;
border: 1px solid #aaa;
}
.dropzone .dz-preview .dz-progress .dz-upload {
display: block;
height: 100%;
width: 0;
background: green;
}
.dropzone .dz-preview .dz-error-message {
color: red;
display: none;
}
.dropzone .dz-preview.dz-error .dz-error-message {
display: block;
}
.dropzone .dz-preview.dz-error .dz-error-mark {
display: block;
filter: drop-shadow(0px 0px 2px red);
}
.dropzone .dz-preview.dz-success .dz-success-mark {
display: block;
filter: drop-shadow(0px 0px 2px green);
}
.dropzone .dz-preview .dz-error-mark, .dropzone .dz-preview .dz-success-mark {
position: absolute;
display: none;
left: 30px;
top: 30px;
width: 54px;
height: 58px;
left: 50%;
margin-left: -27px;
}

View File

@ -22,6 +22,9 @@
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js">
</script>
<div class="content"> <div class="content">
<div class="header"> <div class="header">
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Running</span> <span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">Service Running</span>

View File

@ -37,7 +37,7 @@
<input type="hidden" name="size" value="{{hd.size}}"> <input type="hidden" name="size" value="{{hd.size}}">
<input type="hidden" name="file_type" value="{{hd.file_type}}"> <input type="hidden" name="file_type" value="{{hd.file_type}}">
<label for="file_name">Save as:</label> <label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{hd.name}}" />.{{hd.file_type}} <input type="text" name="file_name" value="{{hd.secure_name}}" />.{{hd.file_type}}
<input type="submit" value="Create" /> <input type="submit" value="Create" />
</form> </form>
</td> </td>
@ -127,7 +127,7 @@
<input type="hidden" name="size" value="{{rm.size}}"> <input type="hidden" name="size" value="{{rm.size}}">
<input type="hidden" name="file_type" value="{{rm.file_type}}"> <input type="hidden" name="file_type" value="{{rm.file_type}}">
<label for="file_name">Save as:</label> <label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{rm.name}}" />.{{rm.file_type}} <input type="text" name="file_name" value="{{rm.secure_name}}" />.{{rm.file_type}}
<input type="submit" value="Create" /> <input type="submit" value="Create" />
</form> </form>
</td> </td>
@ -135,5 +135,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p><small>Available disk space on the Pi: {{free_disk}} MB</small></p>
<p><a href="javascript:history.back()">Cancel</a></p>
{% endblock %} {% endblock %}

View File

@ -91,10 +91,10 @@
</tr> </tr>
{% for file in files %} {% for file in files %}
<tr> <tr>
<td>{{file["name"].replace(base_dir, '')}}</td> <td>{{file["name"]}}</td>
<td style="text-align:center"> <td style="text-align:center">
<form action="/files/download" method="post"> <form action="/files/download" method="post">
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}"> <input type="hidden" name="image" value="{{file["name"]}}">
<input type="submit" value="{{file["size_mb"]}} MB &#8595;" /> <input type="submit" value="{{file["size_mb"]}} MB &#8595;" />
</form> </form>
</td> </td>
@ -107,28 +107,20 @@
<option value="{{id}}">{{id}}</option> <option value="{{id}}">{{id}}</option>
{% endfor %} {% endfor %}
</select> </select>
{% if not file["name"].lower().endswith(archive_file_suffix) %}
<input type="submit" value="Attach" /> <input type="submit" value="Attach" />
{% endif %}
</form> </form>
<form action="/files/delete" method="post" onsubmit="return confirm('Delete file?')"> <form action="/files/delete" method="post" onsubmit="return confirm('Delete file?')">
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}"> <input type="hidden" name="image" value="{{file["name"]}}">
<input type="submit" value="Delete" /> <input type="submit" value="Delete" />
</form> </form>
{% if file["name"].lower().endswith(archive_file_suffix) %}
<form action="/files/unzip" method="post">
<input type="hidden" name="image" value="{{file["name"].replace(base_dir, '')}}">
<input type="submit" value="Unzip" />
</form>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<p><small>Supported file types: {{valid_file_suffix|string()}}</small></p>
<hr/> <hr/>
<h2>Attach Ethernet Adapter</h2> <h2>Attach Ethernet Adapter</h2>
<p>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers required.</a></p> <p>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers required.</a></p>
<p>If you have a DHCP setup, ignore the Static IP fields.</p> <p>If you have a DHCP setup, ignore the Static IP fields.</p>
@ -167,33 +159,37 @@
</table> </table>
<hr/> <hr/>
<h2>Upload File</h2> <h2>Upload File</h2>
<p>Uploads file to <tt>{{base_dir}}</tt>. Max file size is set to {{max_file_size / 1024 /1024 }}MB</p> <p>Uploads file to <tt>{{base_dir}}</tt>. The largest file size accepted is {{max_file_size}} MB. Zip files will be unzipped.</p>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form id="uploadForm" action="/files/upload/" onchange="fileSelect(event)" method="post" enctype="multipart/form-data"> <form method="POST" action="/files/upload" class="dropzone dz-clickable" id="dropper" enctype="multipart/form-data">
<label for="file">File:</label>
<input type="file" name="file"/>
<input type="submit" value="Upload" />
</form> </form>
</td> </td>
</tr> </tr>
</table> </table>
<script> <script type="application/javascript">
function fileSelect(e) { Dropzone.options.dropper = {
document.getElementById("uploadForm").setAttribute('action', "/files/upload/" + e.target.files[0].name) paramName: 'file',
console.log(e.target.files[0].name); acceptedFiles: '{{valid_file_suffix}}',
chunking: true,
forceChunking: true,
url: '/files/upload',
maxFilesize: {{max_file_size}}, // MB
chunkSize: 1000000 // bytes
} }
</script> </script>
<p><small>Recognized file types: {{valid_file_suffix}}</small></p>
<hr/> <hr/>
<h2>Download File from Web</h2> <h2>Download File from Web</h2>
<p>Given a URL, download that file to the <tt>{{base_dir}}</tt></p> <p>Given a URL, download that file to the <tt>{{base_dir}}</tt></p>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
<form action="/files/download_image" method="post"> <form action="/files/download_image" method="post">
<label for="url">URL:</label> <label for="url">URL:</label>
<input type="text" placeholder="URL" name="url" /> <input type="text" placeholder="URL" name="url" />
@ -206,7 +202,7 @@
<hr/> <hr/>
<h2>Download File from web and create HFS CD (Macintosh)</h2> <h2>Download File from web and create HFS CD (Macintosh)</h2>
<p>Given a URL this will download a file, create a HFS iso, and mount it on the device id given. Requires a <a href="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images">compatible CD-ROM driver</a>.</p> <p>Given a URL this will download a file, create a HFS iso, and mount it on the SCSI ID given. Requires a <a href="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images">compatible CD-ROM driver</a>.</p>
<table style="border: none"> <table style="border: none">
<tr style="border: none"> <tr style="border: none">
<td style="border: none; vertical-align:top;"> <td style="border: none; vertical-align:top;">
@ -251,6 +247,7 @@
</td> </td>
</tr> </tr>
</table> </table>
<p><small>Available disk space on the Pi: {{free_disk}} MB</small></p>
<hr/> <hr/>

View File

@ -1,4 +1,15 @@
from flask import Flask, render_template, request, flash, url_for, redirect, send_file, send_from_directory import logging
from flask import (
Flask,
render_template,
request,
flash,
url_for,
redirect,
send_file,
send_from_directory,
make_response,
)
from file_cmds import ( from file_cmds import (
list_files, list_files,
@ -19,6 +30,7 @@ from pi_cmds import (
running_env, running_env,
rascsi_service, rascsi_service,
is_bridge_setup, is_bridge_setup,
disk_space,
) )
from ractl_cmds import ( from ractl_cmds import (
attach_image, attach_image,
@ -41,6 +53,7 @@ app = Flask(__name__)
@app.route("/") @app.route("/")
def index(): def index():
server_info = get_server_info() server_info = get_server_info()
disk = disk_space()
devices = list_devices() devices = list_devices()
files=list_files() files=list_files()
config_files=list_config_files() config_files=list_config_files()
@ -51,6 +64,7 @@ def index():
reserved_scsi_ids = server_info["reserved_ids"] reserved_scsi_ids = server_info["reserved_ids"]
formatted_devices = sort_and_format_devices(devices["device_list"]) formatted_devices = sort_and_format_devices(devices["device_list"])
scsi_ids = get_valid_scsi_ids(devices["device_list"], reserved_scsi_ids) scsi_ids = get_valid_scsi_ids(devices["device_list"], reserved_scsi_ids)
return render_template( return render_template(
"index.html", "index.html",
bridge_configured=is_bridge_setup(), bridge_configured=is_bridge_setup(),
@ -60,11 +74,12 @@ def index():
base_dir=base_dir, base_dir=base_dir,
scsi_ids=scsi_ids, scsi_ids=scsi_ids,
reserved_scsi_ids=reserved_scsi_ids, reserved_scsi_ids=reserved_scsi_ids,
max_file_size=MAX_FILE_SIZE, max_file_size=int(MAX_FILE_SIZE / 1024 / 1024),
running_env=running_env(), running_env=running_env(),
server_info=server_info, server_info=server_info,
netinfo=get_network_info(), netinfo=get_network_info(),
valid_file_suffix=VALID_FILE_SUFFIX, free_disk=int(disk["free"] / 1024 / 1024),
valid_file_suffix="."+", .".join(VALID_FILE_SUFFIX),
removable_device_types=REMOVABLE_DEVICE_TYPES, removable_device_types=REMOVABLE_DEVICE_TYPES,
harddrive_file_suffix=HARDDRIVE_FILE_SUFFIX, harddrive_file_suffix=HARDDRIVE_FILE_SUFFIX,
cdrom_file_suffix=CDROM_FILE_SUFFIX, cdrom_file_suffix=CDROM_FILE_SUFFIX,
@ -79,6 +94,7 @@ def drive_list():
Sets up the data structures and kicks off the rendering of the drive list page Sets up the data structures and kicks off the rendering of the drive list page
""" """
server_info = get_server_info() server_info = get_server_info()
disk = disk_space()
# Reads the canonical drive properties into a dict # Reads the canonical drive properties into a dict
# The file resides in the current dir of the web ui process # The file resides in the current dir of the web ui process
@ -98,14 +114,17 @@ def drive_list():
cd_conf = [] cd_conf = []
rm_conf = [] rm_conf = []
from werkzeug.utils import secure_filename
for d in conf: for d in conf:
if d["device_type"] == "SCHD": if d["device_type"] == "SCHD":
d["secure_name"] = secure_filename(d["name"])
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024) d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
hd_conf.append(d) hd_conf.append(d)
elif d["device_type"] == "SCCD": elif d["device_type"] == "SCCD":
d["size_mb"] = "N/A" d["size_mb"] = "N/A"
cd_conf.append(d) cd_conf.append(d)
elif d["device_type"] == "SCRM": elif d["device_type"] == "SCRM":
d["secure_name"] = secure_filename(d["name"])
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024) d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
rm_conf.append(d) rm_conf.append(d)
@ -124,6 +143,7 @@ def drive_list():
rm_conf=rm_conf, rm_conf=rm_conf,
running_env=running_env(), running_env=running_env(),
server_info=server_info, server_info=server_info,
free_disk=int(disk["free"] / 1024 / 1024),
cdrom_file_suffix=CDROM_FILE_SUFFIX, cdrom_file_suffix=CDROM_FILE_SUFFIX,
) )
@ -142,7 +162,7 @@ def drive_create():
size = request.form.get("size") size = request.form.get("size")
file_type = request.form.get("file_type") file_type = request.form.get("file_type")
file_name = request.form.get("file_name") file_name = request.form.get("file_name")
# Creating the image file # Creating the image file
process = create_new_image(file_name, file_type, size) process = create_new_image(file_name, file_type, size)
if process["status"] == True: if process["status"] == True:
@ -464,29 +484,52 @@ def download_img():
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route("/files/upload/<filename>", methods=["POST"]) @app.route("/files/upload", methods=["POST"])
def upload_file(filename): def upload_file():
if not filename: from werkzeug.utils import secure_filename
flash("No file provided.", "error")
return redirect(url_for("index"))
from os import path from os import path
file_path = path.join(app.config["UPLOAD_FOLDER"], filename) import pydrop
if path.isfile(file_path):
flash(f"{filename} already exists.", "error")
return redirect(url_for("index"))
from io import DEFAULT_BUFFER_SIZE log = logging.getLogger("pydrop")
binary_new_file = "bx" file = request.files["file"]
with open(file_path, binary_new_file, buffering=DEFAULT_BUFFER_SIZE) as f: filename = secure_filename(file.filename)
chunk_size = DEFAULT_BUFFER_SIZE
while True: save_path = path.join(app.config["UPLOAD_FOLDER"], filename)
chunk = request.stream.read(chunk_size) current_chunk = int(request.form['dzchunkindex'])
if len(chunk) == 0:
break # Makes sure not to overwrite an existing file,
f.write(chunk) # but continues writing to a file transfer in progress
# TODO: display an informative success message if path.exists(save_path) and current_chunk == 0:
return redirect(url_for("index", filename=filename)) return make_response((f"The file {file.filename} already exists!", 400))
try:
with open(save_path, "ab") as f:
f.seek(int(request.form["dzchunkbyteoffset"]))
f.write(file.stream.read())
except OSError:
log.exception("Could not write to file")
return make_response(("Unable to write the file to disk!", 500))
total_chunks = int(request.form["dztotalchunkcount"])
if current_chunk + 1 == total_chunks:
# Validate the resulting file size after writing the last chunk
if path.getsize(save_path) != int(request.form["dztotalfilesize"]):
log.error(f"Finished transferring {file.filename}, "
f"but it has a size mismatch with the original file."
f"Got {path.getsize(save_path)} but we "
f"expected {request.form['dztotalfilesize']}.")
return make_response(("Transferred file corrupted!", 500))
else:
log.info(f"File {file.filename} has been uploaded successfully")
if filename.lower().endswith(".zip"):
unzip_file(filename)
else:
log.debug(f"Chunk {current_chunk + 1} of {total_chunks} "
f"for file {file.filename} completed.")
return make_response(("File upload successful!", 200))
@app.route("/files/create", methods=["POST"]) @app.route("/files/create", methods=["POST"])
@ -495,6 +538,9 @@ def create_file():
size = (int(request.form.get("size")) * 1024 * 1024) size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type") file_type = request.form.get("type")
from werkzeug.utils import secure_filename
file_name = secure_filename(file_name)
process = create_new_image(file_name, file_type, size) process = create_new_image(file_name, file_type, size)
if process["status"] == True: if process["status"] == True:
flash(f"Drive image created as {file_name}.{file_type}") flash(f"Drive image created as {file_name}.{file_type}")
@ -543,19 +589,6 @@ def delete():
return redirect(url_for("index")) return redirect(url_for("index"))
@app.route("/files/unzip", methods=["POST"])
def unzip():
image = request.form.get("image")
if unzip_file(image):
flash("Unzipped file " + image)
return redirect(url_for("index"))
else:
flash("Failed to unzip " + image, "error")
return redirect(url_for("index"))
if __name__ == "__main__": if __name__ == "__main__":
app.secret_key = "rascsi_is_awesome_insecure_secret_key" app.secret_key = "rascsi_is_awesome_insecure_secret_key"
app.config["SESSION_TYPE"] = "filesystem" app.config["SESSION_TYPE"] = "filesystem"