Create SysCmds common class, and refactor Python codebase (#697)

* Move the oled script's PiCmds module to common, and rename it SysCmds.

* Use sys_cmds.get_ip_and_host() in web UI code.

* Move the auth_active() method to device_utils

* Rename device_utils to web_utils. Make auth_active() method take the group as argument.

* Migrate all pi_cmds methods to the SysCmds common class.

* Display hostname and ip in Web UI.

* Resolve or suppress pylint warnings.

* Resolve a pylint warning.

* Resolve or suppress pylint warnings.

* Import libraries at the top level for readability. In my testing on a Pi3B+, this leads to ~1.5k more memory being used by the python3 process.

* Change page title as requested by akuker.

* Reenable the import-outside-toplevel pylint rule.

* Resolve pylint warnings.

* Fix error following refactoring.

* Minor UI tweaks.

* Cleanup.

* Break out bridge config validation into a utility method.

* Move the dropzonejs method into the web_utils package

* Move get_logs method into SysCmds class.

* Improve get logs UI.

* Resolve pylint warning.

* Standardize class instance name.
This commit is contained in:
Daniel Markstedt 2022-02-26 21:46:35 -08:00 committed by GitHub
parent 4178d4b845
commit e8f392c3f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 502 additions and 479 deletions

View File

@ -140,8 +140,7 @@ disable=print-statement,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
import-outside-toplevel
comprehension-escape
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View File

@ -4,8 +4,16 @@ Module for methods reading from and writing to the file system
import os
import logging
from pathlib import PurePath
import asyncio
from pathlib import PurePath
from zipfile import ZipFile, is_zipfile
from re import escape, findall
from time import time
from subprocess import run, CalledProcessError
from json import dump, load
import requests
import rascsi_interface_pb2 as proto
from rascsi.common_settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
from rascsi.ractl_cmds import RaCtlCmds
@ -79,7 +87,6 @@ class FileCmds:
prop_data = self.list_files(PROPERTIES_SUFFIX, CFG_DIR)
prop_files = [PurePath(x[0]).stem for x in prop_data]
from zipfile import ZipFile, is_zipfile
server_info = self.ractl.get_server_info()
files = []
for file in result.image_files_info.image_files:
@ -225,12 +232,11 @@ class FileCmds:
members contains all of the full paths to each of the zip archive members
Returns (dict) with (boolean) status and (list of str) msg
"""
from asyncio import run
server_info = self.ractl.get_server_info()
prop_flag = False
if not member:
unzip_proc = run(self.run_async(
unzip_proc = asyncio.run(self.run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name}"
))
@ -241,14 +247,13 @@ class FileCmds:
self.rename_file(f"{server_info['image_dir']}/{name}", f"{CFG_DIR}/{name}")
prop_flag = True
else:
from re import escape
member = escape(member)
unzip_proc = run(self.run_async(
unzip_proc = asyncio.run(self.run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name} {member}"
))
# Attempt to unzip a properties file in the same archive dir
unzip_prop = run(self.run_async(
unzip_prop = asyncio.run(self.run_async(
f"unzip -d {CFG_DIR} -n -j "
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
))
@ -258,7 +263,6 @@ class FileCmds:
logging.warning("Unzipping failed: %s", unzip_proc["stderr"])
return {"status": False, "msg": unzip_proc["stderr"]}
from re import findall
unzipped = findall(
"(?:inflating|extracting):(.+)\n",
unzip_proc["stdout"]
@ -270,8 +274,6 @@ class FileCmds:
Takes (str) url and one or more (str) *iso_args
Returns (dict) with (bool) status and (str) msg
"""
from time import time
from subprocess import run, CalledProcessError
server_info = self.ractl.get_server_info()
@ -287,7 +289,6 @@ class FileCmds:
if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]}
from zipfile import is_zipfile, ZipFile
if is_zipfile(tmp_full_path):
if "XtraStuf.mac" in str(ZipFile(tmp_full_path).namelist()):
logging.info("MacZip file format detected. Will not unzip to retain resource fork.")
@ -339,7 +340,6 @@ class FileCmds:
Takes (str) url, (str) save_dir, (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
import requests
logging.info("Making a request to download %s", url)
try:
@ -371,7 +371,6 @@ class FileCmds:
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, "w", encoding="ISO-8859-1") as json_file:
@ -433,7 +432,6 @@ class FileCmds:
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import load
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, encoding="ISO-8859-1") as json_file:
@ -512,7 +510,6 @@ class FileCmds:
Takes file name base (str) and (list of dicts) conf as arguments
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_path = f"{CFG_DIR}/{file_name}"
try:
with open(file_path, "w") as json_file:
@ -548,7 +545,6 @@ class FileCmds:
Takes (str) file_path as argument.
Returns (dict) with (bool) status, (str) msg, (dict) conf
"""
from json import load
try:
with open(file_path) as json_file:
conf = load(json_file)

View File

@ -162,7 +162,7 @@ class RaCtlCmds:
def get_disk_device_types(self):
"""
Returns a (list) of (str) of four letter device acronyms
Returns a (list) of (str) of four letter device acronyms
that take image files as arguments.
"""
device_types = self.get_device_types()

View File

@ -3,7 +3,10 @@ Module for sending and receiving data over a socket connection with the RaSCSI b
"""
import logging
import socket
from time import sleep
from struct import pack, unpack
from rascsi.exceptions import (EmptySocketChunkException,
InvalidProtobufResponse,
FailedSocketConnectionException)
@ -27,7 +30,6 @@ class SocketCmds:
tries = 20
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@ -56,7 +58,6 @@ class SocketCmds:
Tries to extract and interpret the protobuf header to get response size.
Reads data from socket in 2048 bytes chunks until all data is received.
"""
from struct import pack, unpack
# Sending the magic word "RASCSI" to authenticate with the server
sock.send(b"RASCSI")

View File

@ -0,0 +1,170 @@
"""
Module with methods that interact with the Pi system
"""
import subprocess
import logging
from subprocess import run
from shutil import disk_usage
from re import findall, match
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
class SysCmds:
"""
Class for commands sent to the Pi's Linux system.
"""
@staticmethod
def running_env():
"""
Returns (str) git and (str) env
git contains the git hash of the checked out code
env is the various system information where this app is running
"""
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
ra_git_version = ""
try:
pi_version = (
subprocess.run(
["uname", "-a"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
return {"git": ra_git_version, "env": pi_version}
@staticmethod
def running_proc(daemon):
"""
Takes (str) daemon
Returns (int) proc, which is the number of processes currently running
"""
try:
processes = (
subprocess.run(
["ps", "aux"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
processes = ""
matching_processes = findall(daemon, processes)
return len(matching_processes)
@staticmethod
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
try:
bridges = (
subprocess.run(
["brctl", "show"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
return True
return False
@staticmethod
def disk_space():
"""
Returns a (dict) with (int) total (int) used (int) free
This is the disk space information of the volume where this app is running
"""
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}
@staticmethod
def introspect_file(file_path, re_term):
"""
Takes a (str) file_path and (str) re_term in regex format
Will introspect file_path for the existance of re_term
and return True if found, False if not found
"""
try:
ifile = open(file_path, "r", encoding="ISO-8859-1")
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return False
for line in ifile:
if match(re_term, line):
return True
return False
# pylint: disable=broad-except
@staticmethod
def get_ip_and_host():
"""
Use a mock socket connection to identify the Pi's hostname and IP address
"""
host = gethostname()
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = False
finally:
sock.close()
return ip_addr, host
@staticmethod
def get_logs(lines, scope):
"""
Takes (int) lines and (str) scope.
Returns either the decoded log output, or the stderr output of journalctl.
"""
if scope != "all":
process = run(
["journalctl", "-n", lines, "-u", scope],
capture_output=True,
check=True,
)
else:
process = run(
["journalctl", "-n", lines],
capture_output=True,
check=True,
)
if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8")
return process.returncode, process.stderr.decode("utf-8")

View File

@ -1,21 +0,0 @@
"""
Module with methods that interact with the Pi's Linux system
"""
def get_ip_and_host():
"""
Use a mock socket connection to identify the Pi's hostname and IP address
"""
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
host = gethostname()
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = False
finally:
sock.close()
return ip_addr, host

View File

@ -39,9 +39,9 @@ from board import I2C
from adafruit_ssd1306 import SSD1306_I2C
from PIL import Image, ImageDraw, ImageFont
from interrupt_handler import GracefulInterruptHandler
from pi_cmds import get_ip_and_host
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds
from rascsi.sys_cmds import SysCmds
parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
parser.add_argument(
@ -99,6 +99,7 @@ TOKEN = args.password
sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=TOKEN)
sys_cmd = SysCmds()
WIDTH = 128
BORDER = 5
@ -159,7 +160,7 @@ LINE_SPACING = 8
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
FONT = ImageFont.truetype('resources/type_writer.ttf', FONT_SIZE)
IP_ADDR, HOSTNAME = get_ip_and_host()
IP_ADDR, HOSTNAME = sys_cmd.get_ip_and_host()
REMOVABLE_DEVICE_TYPES = ractl_cmd.get_removable_device_types()
PERIPHERAL_DEVICE_TYPES = ractl_cmd.get_peripheral_device_types()
@ -183,7 +184,7 @@ def formatted_output():
else:
output.append(f"{line['id']} {line['device_type'][2:4]} {line['status']}")
# Special handling of devices that don't use image files
elif line["device_type"] in (PERIPHERAL_DEVICE_TYPES):
elif line["device_type"] in PERIPHERAL_DEVICE_TYPES:
if line["vendor"] == "RaSCSI":
output.append(f"{line['id']} {line['device_type'][2:4]} {line['product']}")
else:

View File

@ -1,90 +0,0 @@
"""
Module for RaSCSI device management utility methods
"""
from flask_babel import _
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids]
unoccupied_ids.sort()
valid_ids = [i for i in list(range(8)) if i not in reserved_ids]
valid_ids.sort(reverse=True)
if unoccupied_ids:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def sort_and_format_devices(devices):
"""
Takes a (list) of (dict)s devices and returns a (list) of (dict)s.
Sorts by SCSI ID acending (0 to 7).
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
def map_device_types_and_names(device_types):
"""
Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types()
Returns a (dict) of device_type:device_name mappings of localized device names
"""
for key, value in device_types.items():
device_types[key]["name"] = get_device_name(key)
return device_types
def get_device_name(device_type):
"""
Takes a four letter device acronym (str) device_type.
Returns the human-readable name for the device type.
"""
if device_type == "SAHD":
return _("SASI Hard Disk")
elif device_type == "SCHD":
return _("SCSI Hard Disk")
elif device_type == "SCRM":
return _("Removable Disk")
elif device_type == "SCMO":
return _("Magneto-Optical")
elif device_type == "SCCD":
return _("CD / DVD")
elif device_type == "SCBR":
return _("X68000 Host Bridge")
elif device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
elif device_type == "SCLP":
return _("Printer")
elif device_type == "SCHS":
return _("Host Services")
else:
return device_type

View File

@ -1,156 +0,0 @@
"""
Module for methods controlling and getting information about the Pi's Linux system
"""
import subprocess
import logging
from flask_babel import _
from settings import AUTH_GROUP
def running_env():
"""
Returns (str) git and (str) env
git contains the git hash of the checked out code
env is the various system information where this app is running
"""
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
ra_git_version = ""
try:
pi_version = (
subprocess.run(
["uname", "-a"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
return {"git": ra_git_version, "env": pi_version}
def running_proc(daemon):
"""
Takes (str) daemon
Returns (int) proc, which is the number of processes currently running
"""
try:
processes = (
subprocess.run(
["ps", "aux"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
processes = ""
from re import findall
matching_processes = findall(daemon, processes)
return len(matching_processes)
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
try:
bridges = (
subprocess.run(
["brctl", "show"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
return True
return False
def disk_space():
"""
Returns a (dict) with (int) total (int) used (int) free
This is the disk space information of the volume where this app is running
"""
from shutil import disk_usage
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}
def get_ip_address():
"""
Use a mock socket connection to identify the Pi's IP address
"""
from socket import socket, AF_INET, SOCK_DGRAM
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = '127.0.0.1'
finally:
sock.close()
return ip_addr
def introspect_file(file_path, re_term):
"""
Takes a (str) file_path and (str) re_term in regex format
Will introspect file_path for the existance of re_term
and return True if found, False if not found
"""
from re import match
try:
ifile = open(file_path, "r", encoding="ISO-8859-1")
except:
return False
for line in ifile:
if match(re_term, line):
return True
return False
def auth_active():
"""
Inspects if the group defined in AUTH_GROUP exists on the system.
If it exists, tell the webapp to enable authentication.
Returns a (dict) with (bool) status and (str) msg
"""
from grp import getgrall
groups = [g.gr_name for g in getgrall()]
if AUTH_GROUP in groups:
return {
"status": True,
"msg": _("You must log in to use this function"),
}
return {"status": False, "msg": ""}

View File

@ -41,5 +41,8 @@ class ReturnCodeMapper:
parameters = payload["parameters"]
payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]], **parameters)
payload["msg"] = lazy_gettext(
ReturnCodeMapper.MESSAGES[payload["return_code"]],
**parameters,
)
return payload

View File

@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
<title>{{ _("RaSCSI Control Page") }}</title>
<title>{{ _("RaSCSI Control Page") }} [{{ host }}]</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
@ -52,22 +52,27 @@
<div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="{{ _("Password") }}">
<input type="submit" value="Login">
<input type="submit" value="Login">
</form>
</span>
{% endif %}
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} &#8211; {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
{% endif %}
<table width="100%">
<table width="100%" style="background-color: black;">
<tbody>
<tr style="background-color: black;">
<td style="background-color: black;">
<tr align="center">
<td>
<a href="http://github.com/akuker/RASCSI" target="_blank">
<h1>RaSCSI - 68kmla Edition</h1>
<h1>{{ _("RaSCSI Control Page") }}</h1>
</a>
</td>
</tr>
<tr>
<td style="color: white;">
hostname: {{ host }} ip: {{ ip_addr }}
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -65,7 +65,7 @@
<input name="type" type="hidden" value="{{ device.device_type }}">
<input name="file_size" type="hidden" value="{{ device.size }}">
<select type="select" name="file_name">
{% for f in files %}
{% for f in files|sort(attribute='name') %}
{% if device.device_type == "SCCD" %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
@ -601,11 +601,11 @@
<td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post">
<label for="lines">{{ _("Log Lines:") }}</label>
<input name="lines" type="number" placeholder="200" min="1" size="4">
<input name="lines" type="number" value="200" min="1" size="4">
<label for="scope">{{ _("Scope:") }}</label>
<select name="scope">
<option value="default">
default
<option value="all">
all
</option>
<option value="rascsi">
rascsi.service
@ -613,6 +613,9 @@
<option value="rascsi-web">
rascsi-web.service
</option>
<option value="rascsi-oled">
rascsi-oled.service
</option>
</select>
<input type="submit" value="{{ _("Show Logs") }}">
</form>

View File

@ -6,6 +6,13 @@ import logging
import argparse
from pathlib import Path
from functools import wraps
from grp import getgrall
from ast import literal_eval
import bjoern
from werkzeug.utils import secure_filename
from simplepam import authenticate
from flask_babel import Babel, Locale, refresh, _
from flask import (
Flask,
@ -20,26 +27,30 @@ from flask import (
session,
abort,
)
from flask_babel import Babel, Locale, refresh, _
from pi_cmds import (
running_env,
running_proc,
is_bridge_setup,
disk_space,
get_ip_address,
introspect_file,
auth_active,
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.file_cmds import FileCmds
from rascsi.sys_cmds import SysCmds
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
RESERVATIONS,
)
from device_utils import (
from return_code_mapper import ReturnCodeMapper
from socket_cmds_flask import SocketCmdsFlask
from web_utils import (
sort_and_format_devices,
get_valid_scsi_ids,
map_device_types_and_names,
get_device_name,
auth_active,
is_bridge_configured,
upload_with_dropzonejs,
)
from return_code_mapper import ReturnCodeMapper
from settings import (
AFP_DIR,
MAX_FILE_SIZE,
@ -50,17 +61,6 @@ from settings import (
LANGUAGES,
)
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
RESERVATIONS,
)
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.file_cmds import FileCmds
from socket_cmds_flask import SocketCmdsFlask
APP = Flask(__name__)
BABEL = Babel(APP)
@ -93,12 +93,13 @@ def get_supported_locales():
return sorted_locales
# pylint: disable=too-many-locals
@APP.route("/")
def index():
"""
Sets up data structures for and renders the index page
"""
if not ractl.is_token_auth()["status"] and not APP.config["TOKEN"]:
if not ractl_cmd.is_token_auth()["status"] and not APP.config["TOKEN"]:
abort(
403,
_(
@ -107,11 +108,12 @@ def index():
),
)
server_info = ractl.get_server_info()
devices = ractl.list_devices()
device_types = map_device_types_and_names(ractl.get_device_types()["device_types"])
image_files = file_cmds.list_images()
config_files = file_cmds.list_config_files()
server_info = ractl_cmd.get_server_info()
devices = ractl_cmd.list_devices()
device_types = map_device_types_and_names(ractl_cmd.get_device_types()["device_types"])
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"]:
@ -147,10 +149,11 @@ def index():
return render_template(
"index.html",
locales=get_supported_locales(),
bridge_configured=is_bridge_setup(),
netatalk_configured=running_proc("afpd"),
macproxy_configured=running_proc("macproxy"),
ip_addr=get_ip_address(),
bridge_configured=sys_cmd.is_bridge_setup(),
netatalk_configured=sys_cmd.running_proc("afpd"),
macproxy_configured=sys_cmd.running_proc("macproxy"),
ip_addr=ip_addr,
host=host,
devices=formatted_devices,
files=extended_image_files,
config_files=config_files,
@ -165,24 +168,24 @@ def index():
reserved_scsi_ids=reserved_scsi_ids,
RESERVATIONS=RESERVATIONS,
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
running_env=running_env(),
running_env=sys_cmd.running_env(),
version=server_info["version"],
log_levels=server_info["log_levels"],
current_log_level=server_info["current_log_level"],
netinfo=ractl.get_network_info(),
netinfo=ractl_cmd.get_network_info(),
device_types=device_types,
free_disk=int(disk_space()["free"] / 1024 / 1024),
free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024),
valid_file_suffix=valid_file_suffix,
cdrom_file_suffix=tuple(server_info["sccd"]),
removable_file_suffix=tuple(server_info["scrm"]),
mo_file_suffix=tuple(server_info["scmo"]),
username=username,
auth_active=auth_active()["status"],
auth_active=auth_active(AUTH_GROUP)["status"],
ARCHIVE_FILE_SUFFIX=ARCHIVE_FILE_SUFFIX,
PROPERTIES_SUFFIX=PROPERTIES_SUFFIX,
REMOVABLE_DEVICE_TYPES=ractl.get_removable_device_types(),
DISK_DEVICE_TYPES=ractl.get_disk_device_types(),
PERIPHERAL_DEVICE_TYPES=ractl.get_peripheral_device_types(),
REMOVABLE_DEVICE_TYPES=ractl_cmd.get_removable_device_types(),
DISK_DEVICE_TYPES=ractl_cmd.get_disk_device_types(),
PERIPHERAL_DEVICE_TYPES=ractl_cmd.get_peripheral_device_types(),
)
@ -195,7 +198,7 @@ def drive_list():
# The file resides in the current dir of the web ui process
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if drive_properties.is_file():
process = file_cmds.read_drive_properties(str(drive_properties))
process = file_cmd.read_drive_properties(str(drive_properties))
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
@ -215,7 +218,6 @@ def drive_list():
cd_conf = []
rm_conf = []
from werkzeug.utils import secure_filename
for device in conf:
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
@ -234,21 +236,21 @@ def drive_list():
else:
username = None
server_info = ractl.get_server_info()
server_info = ractl_cmd.get_server_info()
return render_template(
"drives.html",
files=file_cmds.list_images()["files"],
files=file_cmd.list_images()["files"],
base_dir=server_info["image_dir"],
hd_conf=hd_conf,
cd_conf=cd_conf,
rm_conf=rm_conf,
running_env=running_env(),
running_env=sys_cmd.running_env(),
version=server_info["version"],
free_disk=int(disk_space()["free"] / 1024 / 1024),
free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024),
cdrom_file_suffix=tuple(server_info["sccd"]),
username=username,
auth_active=auth_active()["status"],
auth_active=auth_active(AUTH_GROUP)["status"],
)
@ -260,9 +262,6 @@ def login():
username = request.form["username"]
password = request.form["password"]
from simplepam import authenticate
from grp import getgrall
groups = [g.gr_name for g in getgrall() if username in g.gr_mem]
if AUTH_GROUP in groups:
if authenticate(str(username), str(password)):
@ -287,12 +286,12 @@ def logout():
return redirect(url_for("index"))
@APP.route("/pwa/<path:path>")
def send_pwa_files(path):
@APP.route("/pwa/<path:pwa_path>")
def send_pwa_files(pwa_path):
"""
Sets up mobile web resources
"""
return send_from_directory("pwa", path)
return send_from_directory("pwa", pwa_path)
def login_required(func):
@ -301,7 +300,7 @@ def login_required(func):
"""
@wraps(func)
def decorated_function(*args, **kwargs):
auth = auth_active()
auth = auth_active(AUTH_GROUP)
if auth["status"] and "username" not in session:
flash(auth["msg"], "error")
return redirect(url_for("index"))
@ -325,7 +324,7 @@ def drive_create():
full_file_name = file_name + "." + file_type
# Creating the image file
process = file_cmds.create_new_image(file_name, file_type, size)
process = file_cmd.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
else:
@ -340,7 +339,7 @@ def drive_create():
"revision": revision,
"block_size": block_size,
}
process = file_cmds.write_drive_properties(prop_file_name, properties)
process = file_cmd.write_drive_properties(prop_file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -370,7 +369,7 @@ def drive_cdrom():
"revision": revision,
"block_size": block_size,
}
process = file_cmds.write_drive_properties(file_name, properties)
process = file_cmd.write_drive_properties(file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -389,7 +388,7 @@ def config_save():
file_name = request.form.get("name") or "default"
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
process = file_cmds.write_config(file_name)
process = file_cmd.write_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -408,7 +407,7 @@ def config_load():
file_name = request.form.get("name")
if "load" in request.form:
process = file_cmds.read_config(file_name)
process = file_cmd.read_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -417,7 +416,7 @@ def config_load():
flash(process['msg'], "error")
return redirect(url_for("index"))
if "delete" in request.form:
process = file_cmds.delete_file(f"{CFG_DIR}/{file_name}")
process = file_cmd.delete_file(f"{CFG_DIR}/{file_name}")
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -437,28 +436,15 @@ def show_logs():
Displays system logs
"""
lines = request.form.get("lines") or "200"
scope = request.form.get("scope") or "default"
scope = request.form.get("scope") or "all"
from subprocess import run
if scope != "default":
process = run(
["journalctl", "-n", lines, "-u", scope],
capture_output=True,
check=True,
)
else:
process = run(
["journalctl", "-n", lines],
capture_output=True,
check=True,
)
if process.returncode == 0:
returncode, logs = sys_cmd.get_logs(lines, scope)
if returncode == 0:
headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers
return logs, int(lines), headers
flash(_("An error occurred when fetching logs."))
flash(process.stderr.decode("utf-8"), "stderr")
flash(logs, "stderr")
return redirect(url_for("index"))
@ -470,7 +456,7 @@ def log_level():
"""
level = request.form.get("level") or "info"
process = ractl.set_log_level(level)
process = ractl_cmd.set_log_level(level)
if process["status"]:
flash(_("Log level set to %(value)s", value=level))
return redirect(url_for("index"))
@ -502,31 +488,18 @@ def attach_device():
error_msg = _("Please follow the instructions at %(url)s", url=error_url)
if "interface" in params.keys():
if params["interface"].startswith("wlan"):
if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
flash(_("Configure IPv4 forwarding before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
if not Path("/etc/iptables/rules.v4").is_file():
flash(_("Configure NAT before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
else:
if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + params["interface"] + r"$"):
flash(_("Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
flash(_("Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
bridge_status = is_bridge_configured(params["interface"])
if bridge_status:
flash(bridge_status, "error")
flash(error_msg, "error")
return redirect(url_for("index"))
kwargs = {
"unit": int(unit),
"device_type": device_type,
"params": params,
}
process = ractl.attach_device(scsi_id, **kwargs)
process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_(
@ -557,14 +530,14 @@ def attach_image():
if device_type:
kwargs["device_type"] = device_type
device_types = ractl.get_device_types()
device_types = ractl_cmd.get_device_types()
expected_block_size = min(device_types["device_types"][device_type]["block_sizes"])
# Attempt to load the device properties file:
# same file name with PROPERTIES_SUFFIX appended
drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
if Path(drive_properties).is_file():
process = file_cmds.read_drive_properties(drive_properties)
process = file_cmd.read_drive_properties(drive_properties)
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
@ -576,7 +549,7 @@ def attach_image():
kwargs["block_size"] = conf["block_size"]
expected_block_size = conf["block_size"]
process = ractl.attach_device(scsi_id, **kwargs)
process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_((
@ -607,7 +580,7 @@ def detach_all_devices():
"""
Detaches all currently attached devices
"""
process = ractl.detach_all()
process = ractl_cmd.detach_all()
if process["status"]:
flash(_("Detached all SCSI devices"))
return redirect(url_for("index"))
@ -624,7 +597,7 @@ def detach():
"""
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = ractl.detach_by_id(scsi_id, unit)
process = ractl_cmd.detach_by_id(scsi_id, unit)
if process["status"]:
flash(_("Detached SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
@ -645,7 +618,7 @@ def eject():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = ractl.eject_by_id(scsi_id, unit)
process = ractl_cmd.eject_by_id(scsi_id, unit)
if process["status"]:
flash(_("Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
@ -664,7 +637,7 @@ def device_info():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
devices = ractl.list_devices(scsi_id, unit)
devices = ractl_cmd.list_devices(scsi_id, unit)
# First check if any device at all was returned
if not devices["status"]:
@ -700,9 +673,9 @@ def reserve_id():
"""
scsi_id = request.form.get("scsi_id")
memo = request.form.get("memo")
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids = ractl_cmd.get_reserved_ids()["ids"]
reserved_ids.extend(scsi_id)
process = ractl.reserve_scsi_ids(reserved_ids)
process = ractl_cmd.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = memo
flash(_("Reserved SCSI ID %(id_number)s", id_number=scsi_id))
@ -719,9 +692,9 @@ def release_id():
Releases the reservation of a SCSI ID as well as the memo for the reservation
"""
scsi_id = request.form.get("scsi_id")
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids = ractl_cmd.get_reserved_ids()["ids"]
reserved_ids.remove(scsi_id)
process = ractl.reserve_scsi_ids(reserved_ids)
process = ractl_cmd.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = ""
flash(_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
@ -738,7 +711,7 @@ def restart():
"""
Restarts the Pi
"""
ractl.shutdown_pi("reboot")
ractl_cmd.shutdown_pi("reboot")
return redirect(url_for("index"))
@ -748,7 +721,7 @@ def shutdown():
"""
Shuts down the Pi
"""
ractl.shutdown_pi("system")
ractl_cmd.shutdown_pi("system")
return redirect(url_for("index"))
@ -762,7 +735,7 @@ def download_to_iso():
url = request.form.get("url")
iso_args = request.form.get("type").split()
process = file_cmds.download_file_to_iso(url, *iso_args)
process = file_cmd.download_file_to_iso(url, *iso_args)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -772,7 +745,7 @@ def download_to_iso():
flash(process["msg"], "error")
return redirect(url_for("index"))
process_attach = ractl.attach_device(
process_attach = ractl_cmd.attach_device(
scsi_id,
device_type="SCCD",
params={"file": process["file_name"]},
@ -795,8 +768,8 @@ def download_img():
Downloads a remote file onto the images dir on the Pi
"""
url = request.form.get("url")
server_info = ractl.get_server_info()
process = file_cmds.download_to_dir(url, server_info["image_dir"], Path(url).name)
server_info = ractl_cmd.get_server_info()
process = file_cmd.download_to_dir(url, server_info["image_dir"], Path(url).name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -815,7 +788,7 @@ def download_afp():
"""
url = request.form.get("url")
file_name = Path(url).name
process = file_cmds.download_to_dir(url, AFP_DIR, file_name)
process = file_cmd.download_to_dir(url, AFP_DIR, file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -833,55 +806,12 @@ def upload_file():
Depending on the Dropzone.js JavaScript library
"""
# Due to the embedded javascript library, we cannot use the @login_required decorator
auth = auth_active()
auth = auth_active(AUTH_GROUP)
if auth["status"] and "username" not in session:
return make_response(auth["msg"], 403)
from werkzeug.utils import secure_filename
from os import path
log = logging.getLogger("pydrop")
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
server_info = ractl.get_server_info()
save_path = path.join(server_info["image_dir"], file_name)
current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
if path.exists(save_path) and current_chunk == 0:
return make_response(_("The file already exists!"), 400)
try:
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file_object.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(
"Finished transferring %s, "
"but it has a size mismatch with the original file. "
"Got %s but we expected %s.",
file_object.filename,
path.getsize(save_path),
request.form['dztotalfilesize'],
)
return make_response(_("Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file_object.filename)
return make_response(_("File upload successful!"), 200)
server_info = ractl_cmd.get_server_info()
return upload_with_dropzonejs(server_info["image_dir"])
@APP.route("/files/create", methods=["POST"])
@ -895,7 +825,7 @@ def create_file():
file_type = request.form.get("type")
full_file_name = file_name + "." + file_type
process = file_cmds.create_new_image(file_name, file_type, size)
process = file_cmd.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
return redirect(url_for("index"))
@ -922,7 +852,7 @@ def delete():
"""
file_name = request.form.get("file_name")
process = file_cmds.delete_image(file_name)
process = file_cmd.delete_image(file_name)
if process["status"]:
flash(_("Image file deleted: %(file_name)s", file_name=file_name))
else:
@ -932,7 +862,7 @@ def delete():
# Delete the drive properties file, if it exists
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
if Path(prop_file_path).is_file():
process = file_cmds.delete_file(prop_file_path)
process = file_cmd.delete_file(prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
@ -954,7 +884,7 @@ def rename():
file_name = request.form.get("file_name")
new_file_name = request.form.get("new_file_name")
process = file_cmds.rename_image(file_name, new_file_name)
process = file_cmd.rename_image(file_name, new_file_name)
if process["status"]:
flash(_("Image file renamed to: %(file_name)s", file_name=new_file_name))
else:
@ -965,7 +895,7 @@ def rename():
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
new_prop_file_path = f"{CFG_DIR}/{new_file_name}.{PROPERTIES_SUFFIX}"
if Path(prop_file_path).is_file():
process = file_cmds.rename_file(prop_file_path, new_prop_file_path)
process = file_cmd.rename_file(prop_file_path, new_prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -987,11 +917,10 @@ def unzip():
zip_member = request.form.get("zip_member") or False
zip_members = request.form.get("zip_members") or False
from ast import literal_eval
if zip_members:
zip_members = literal_eval(zip_members)
process = file_cmds.unzip_file(zip_file, zip_member, zip_members)
process = file_cmd.unzip_file(zip_file, zip_member, zip_members)
if process["status"]:
if not process["msg"]:
flash(_("Aborted unzip: File(s) with the same name already exists."), "error")
@ -1015,8 +944,8 @@ def change_language():
"""
locale = request.form.get("locale")
session["language"] = locale
ractl.locale = session["language"]
file_cmds.locale = session["language"]
ractl_cmd.locale = session["language"]
file_cmd.locale = session["language"]
refresh()
language = Locale.parse(locale)
@ -1032,8 +961,8 @@ def detect_locale():
This requires the Flask app to have started first.
"""
session["language"] = get_locale()
ractl.locale = session["language"]
file_cmds.locale = session["language"]
ractl_cmd.locale = session["language"]
file_cmd.locale = session["language"]
if __name__ == "__main__":
@ -1074,12 +1003,12 @@ if __name__ == "__main__":
APP.config["TOKEN"] = arguments.password
sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port)
ractl = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"])
file_cmds = FileCmds(sock_cmd=sock_cmd, ractl=ractl, token=APP.config["TOKEN"])
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"])
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["TOKEN"])
sys_cmd = SysCmds()
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
file_cmds.read_config(DEFAULT_CONFIG)
file_cmd.read_config(DEFAULT_CONFIG)
import bjoern
print("Serving rascsi-web...")
bjoern.run(APP, "0.0.0.0", arguments.port)

183
python/web/src/web_utils.py Normal file
View File

@ -0,0 +1,183 @@
"""
Module for RaSCSI Web Interface utility methods
"""
import logging
from grp import getgrall
from os import path
from pathlib import Path
from flask import request, make_response
from flask_babel import _
from werkzeug.utils import secure_filename
from rascsi.sys_cmds import SysCmds
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids]
unoccupied_ids.sort()
valid_ids = [i for i in list(range(8)) if i not in reserved_ids]
valid_ids.sort(reverse=True)
if unoccupied_ids:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def sort_and_format_devices(devices):
"""
Takes a (list) of (dict)s devices and returns a (list) of (dict)s.
Sorts by SCSI ID acending (0 to 7).
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
def map_device_types_and_names(device_types):
"""
Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types()
Returns a (dict) of device_type:device_name mappings of localized device names
"""
for device in device_types.keys():
device_types[device]["name"] = get_device_name(device)
return device_types
# pylint: disable=too-many-return-statements
def get_device_name(device_type):
"""
Takes a four letter device acronym (str) device_type.
Returns the human-readable name for the device type.
"""
if device_type == "SAHD":
return _("SASI Hard Disk")
if device_type == "SCHD":
return _("SCSI Hard Disk")
if device_type == "SCRM":
return _("Removable Disk")
if device_type == "SCMO":
return _("Magneto-Optical")
if device_type == "SCCD":
return _("CD / DVD")
if device_type == "SCBR":
return _("X68000 Host Bridge")
if device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
if device_type == "SCLP":
return _("Printer")
if device_type == "SCHS":
return _("Host Services")
return device_type
def auth_active(group):
"""
Inspects if the group defined in (str) group exists on the system.
If it exists, tell the webapp to enable authentication.
Returns a (dict) with (bool) status and (str) msg
"""
groups = [g.gr_name for g in getgrall()]
if group in groups:
return {
"status": True,
"msg": _("You must log in to use this function"),
}
return {"status": False, "msg": ""}
def is_bridge_configured(interface):
"""
Takes (str) interface of a network device being attached.
Returns (bool) False if the network bridge is configured.
Returns (str) with an error message if the network bridge is not configured.
"""
sys_cmd = SysCmds()
if interface.startswith("wlan"):
if not sys_cmd.introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
return _("Configure IPv4 forwarding before using a wireless network device.")
if not Path("/etc/iptables/rules.v4").is_file():
return _("Configure NAT before using a wireless network device.")
else:
if not sys_cmd.introspect_file(
"/etc/dhcpcd.conf",
r"^denyinterfaces " + interface + r"$",
):
return _("Configure the network bridge before using a wired network device.")
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
return _("Configure the network bridge before using a wired network device.")
return False
def upload_with_dropzonejs(image_dir):
"""
Takes (str) image_dir which is the path to the image dir to store files.
Opens a stream to transfer a file via the embedded dropzonejs library.
"""
log = logging.getLogger("pydrop")
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
save_path = path.join(image_dir, file_name)
current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
if path.exists(save_path) and current_chunk == 0:
return make_response(_("The file already exists!"), 400)
try:
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file_object.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(
"Finished transferring %s, "
"but it has a size mismatch with the original file. "
"Got %s but we expected %s.",
file_object.filename,
path.getsize(save_path),
request.form['dztotalfilesize'],
)
return make_response(_("Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file_object.filename)
return make_response(_("File upload successful!"), 200)