Web UI code cleanup and refactoring (#409)

* Remove dead code

* Clean up indentation

* Cleanup

* Move socket commands into its own file

* Move non-rascsi command methods into its own file

* Refactoring

* Bring back list_config_files

* Cleanup

* Cleanup of status messages

* Remove unused libraries

* Resolve pylint warnings

* Resolve pylint warnings

* Remove unused library

* Resolve pylint warnings

* Clean up status messages

* Add requests lib to requirements.txt

* Clean up status messages

* Resolve interpolation warnings for logging

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Cleanup

* Add html/head/body tags to the base document

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Resolve pylint warnings

* Add .pylintrc and suppress warnings for the generated protobuf module

* Resolve pylint warnings

* Clean up docstrings

* Fix error

* Cleanup

* Add info on pylint to README

* Store .pylintrc in parent dir to allow other Python packages to use it

* Tidy index.html

* Cleanup

* Resolve jinja-ninja warnings

* Cleanup

* Cleanup

* Cleanup

* Cleanup

* Cleanup

* Fix wording
This commit is contained in:
Daniel Markstedt 2021-11-06 17:25:02 -07:00 committed by GitHub
parent 092cb60702
commit 54b3e480a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1885 additions and 1199 deletions

549
src/.pylintrc Normal file
View File

@ -0,0 +1,549 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint.
jobs=1
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
invalid-unicode-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
locally-enabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
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
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=optparse.Values,sys.exit
[BASIC]
# Naming style matching correct argument names
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style
#argument-rgx=
# Naming style matching correct attribute names
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style
#class-attribute-rgx=
# Naming style matching correct class names
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-style
#class-rgx=
# Naming style matching correct constant names
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style
#function-rgx=
# Good variable names which should always be accepted, separated by a comma
good-names=i,
j,
k,
ex,
Run,
_
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming style matching correct inline iteration names
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style
#inlinevar-rgx=
# Naming style matching correct method names
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style
#method-rgx=
# Naming style matching correct module names
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming style matching correct variable names
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style
#variable-rgx=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=rascsi_interface_pb2
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,io,builtins
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,
TERMIOS,
Bastion,
rexec
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

1
src/web/.pylintrc Symbolic link
View File

@ -0,0 +1 @@
../.pylintrc

View File

@ -17,7 +17,18 @@ $ BASE_DIR=/tmp/images/ PATH=$PATH:`pwd`/mock/bin/ python3 web.py
You may edit the files under `mock/bin` to simulate Linux command responses.
TODO: rascsi-web uses protobuf commands to send and receive data from rascsi.
A separate mocking solusion will be needed for this interface.
A separate mocking solution will be needed for this interface.
### Static analysis with pylint
It is recommended to run pylint against new code to protect against bugs
and keep the code readable and maintainable.
The local pylint configuration lives in .pylintrc
```
sudo apt install pylint3
pylint3 python_source_file.py
```
## Pushing to the Pi via git

View File

@ -1,45 +0,0 @@
from machfs import Volume, Folder, File
from settings import *
# Build a cd and attempt to fix resource forks if known
def make_cd(file_path, file_type, file_creator):
with open(file_path, "rb") as f:
file_bytes = f.read()
file_name = file_path.split("/")[-1]
file_suffix = file_name.split(".")[-1]
if file_type is None and file_creator is None:
if file_suffix.lower() == "sea":
file_type = ".sea"
file_creator = "APPL"
v = Volume()
v.name = "TestName"
v["Folder"] = Folder()
v["Folder"][file_name] = File()
v["Folder"][file_name].data = file_bytes
v["Folder"][file_name].rsrc = b""
if not (file_type is None and file_creator is None):
v["Folder"][file_name].type = bytearray(file_type)
v["Folder"][file_name].creator = bytearray(file_creator)
padding = (len(file_bytes) % 512) + (512 * 50)
print("mod", str(len(file_bytes) % 512))
print("padding " + str(padding))
print("len " + str(len(file_bytes)))
print("total " + str(len(file_bytes) + padding))
with open(base_dir + "test.hda", "wb") as f:
flat = v.write(
size=len(file_bytes) + padding,
align=512, # Allocation block alignment modulus (2048 for CDs)
desktopdb=True, # Create a dummy Desktop Database to prevent a rebuild on boot
bootable=False, # This requires a folder with a ZSYS and a FNDR file
startapp=("Folder", file_name), # Path (as tuple) to an app to open at boot
)
f.write(flat)
return base_dir + "test.hda"

49
src/web/device_utils.py Normal file
View File

@ -0,0 +1,49 @@
"""
Module for RaSCSI device management utility methods
"""
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

View File

@ -1,5 +1,10 @@
"""
Module for methods reading from and writing to the file system
"""
import os
import logging
from pathlib import PurePath
from ractl_cmds import (
get_server_info,
@ -8,15 +13,15 @@ from ractl_cmds import (
list_devices,
send_pb_command,
)
from settings import *
from settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX
import rascsi_interface_pb2 as proto
def list_files(file_types, dir_path):
"""
Takes a list or tuple of str file_types - e.g. ('hda', 'hds')
Returns list of lists files_list:
index 0 is str file name and index 1 is int size in bytes
Takes a (list) or (tuple) of (str) file_types - e.g. ('hda', 'hds')
Returns (list) of (list)s files_list:
index 0 is (str) file name and index 1 is (int) size in bytes
"""
files_list = []
for path, dirs, files in os.walk(dir_path):
@ -36,13 +41,13 @@ def list_files(file_types, dir_path):
def list_config_files():
"""
Returns a list of RaSCSI config files in cfg_dir:
list of str files_list
Finds fils with file ending CONFIG_FILE_SUFFIX in CFG_DIR.
Returns a (list) of (str) files_list
"""
files_list = []
for root, dirs, files in os.walk(cfg_dir):
for root, dirs, files in os.walk(CFG_DIR):
for file in files:
if file.endswith(".json"):
if file.endswith("." + CONFIG_FILE_SUFFIX):
files_list.append(file)
return files_list
@ -50,7 +55,7 @@ def list_config_files():
def list_images():
"""
Sends a IMAGE_FILES_INFO command to the server
Returns a dict with boolean status, str msg, and list of dicts files
Returns a (dict) with (bool) status, (str) msg, and (list) of (dict)s files
"""
command = proto.PbCommand()
@ -60,56 +65,53 @@ def list_images():
result = proto.PbResult()
result.ParseFromString(data)
# Get a list of all *.properties files in cfg_dir
from pathlib import PurePath
prop_data = list_files(PROPERTIES_SUFFIX, cfg_dir)
# Get a list of all *.properties files in CFG_DIR
prop_data = 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 = get_server_info()
files = []
for f in result.image_files_info.image_files:
for file in result.image_files_info.image_files:
# Add properties meta data for the image, if applicable
if f.name in prop_files:
process = read_drive_properties(f"{cfg_dir}/{f.name}.{PROPERTIES_SUFFIX}")
if file.name in prop_files:
process = read_drive_properties(f"{CFG_DIR}/{file.name}.{PROPERTIES_SUFFIX}")
prop = process["conf"]
else:
prop = False
if f.name.lower().endswith(".zip"):
zip_path = f"{server_info['image_dir']}/{f.name}"
if file.name.lower().endswith(".zip"):
zip_path = f"{server_info['image_dir']}/{file.name}"
if is_zipfile(zip_path):
zip = ZipFile(zip_path)
# Get a list of str containing all zipfile members
zip_members = zip.namelist()
zipfile = ZipFile(zip_path)
# Get a list of (str) containing all zipfile members
zip_members = zipfile.namelist()
# Strip out directories from the list
zip_members = [x for x in zip_members if not x.endswith("/")]
else:
logging.warning(f"{zip_path} is an invalid zip file")
logging.warning("%s is an invalid zip file", zip_path)
zip_members = False
else:
zip_members = False
size_mb = "{:,.1f}".format(f.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(f.type)
files.append(
{
"name": f.name,
"size": f.size,
"size_mb": size_mb,
"detected_type": dtype,
"prop": prop,
"zip": zip_members,
}
)
size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(file.type)
files.append({
"name": file.name,
"size": file.size,
"size_mb": size_mb,
"detected_type": dtype,
"prop": prop,
"zip": zip_members,
})
return {"status": result.status, "msg": result.msg, "files": files}
def create_new_image(file_name, file_type, size):
"""
Takes str file_name, str file_type, and int size
Takes (str) file_name, (str) file_type, and (int) size
Sends a CREATE_IMAGE command to the server
Returns dict with boolean status and str msg
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.CREATE_IMAGE
@ -126,9 +128,9 @@ def create_new_image(file_name, file_type, size):
def delete_image(file_name):
"""
Takes str file_name
Takes (str) file_name
Sends a DELETE_IMAGE command to the server
Returns dict with boolean status and str msg
Returns (dict) with (bool) status and (str) msg
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DELETE_IMAGE
@ -143,26 +145,25 @@ def delete_image(file_name):
def delete_file(file_path):
"""
Takes str file_path with the full path to the file to delete
Returns dict with boolean status and str msg
Takes (str) file_path with the full path to the file to delete
Returns (dict) with (bool) status and (str) msg
"""
if os.path.exists(file_path):
os.remove(file_path)
return {"status": True, "msg": "File deleted"}
else:
return {"status": False, "msg": "Could not delete file"}
return {"status": True, "msg": f"File deleted: {file_path}"}
return {"status": False, "msg": f"File to delete not found: {file_path}"}
def unzip_file(file_name, member=False):
"""
Takes (str) file_name, optional (str) member
Returns dict with (boolean) status and (list of str) msg
Returns (dict) with (boolean) status and (list of str) msg
"""
from subprocess import run
from re import escape
server_info = get_server_info()
if member == False:
if not member:
unzip_proc = run(
["unzip", "-d", server_info["image_dir"], "-n", "-j", \
f"{server_info['image_dir']}/{file_name}"], capture_output=True
@ -173,26 +174,29 @@ def unzip_file(file_name, member=False):
f"{server_info['image_dir']}/{file_name}", escape(member)], capture_output=True
)
if unzip_proc.returncode != 0:
stderr = unzip_proc.stderr.decode("utf-8")
logging.warning(f"Unzipping failed: {stderr}")
stderr = unzip_proc.stderr.decode("utf-8")
logging.warning("Unzipping failed: %s", stderr)
return {"status": False, "msg": stderr}
from re import findall
unzipped = findall("(?:inflating|extracting):(.+)\n", unzip_proc.stdout.decode("utf-8"))
unzipped = findall(
"(?:inflating|extracting):(.+)\n",
unzip_proc.stdout.decode("utf-8")
)
return {"status": True, "msg": unzipped}
def download_file_to_iso(scsi_id, url):
def download_file_to_iso(url):
"""
Takes int scsi_id and str url
Returns dict with boolean status and str msg
Takes (int) scsi_id and (str) url
Returns (dict) with (bool) status and (str) msg
"""
from time import time
from subprocess import run
server_info = get_server_info()
file_name = url.split("/")[-1]
file_name = PurePath(url).name
tmp_ts = int(time())
tmp_dir = "/tmp/" + str(tmp_ts) + "/"
os.mkdir(tmp_dir)
@ -201,7 +205,7 @@ def download_file_to_iso(scsi_id, url):
req_proc = download_to_dir(url, tmp_dir)
if req_proc["status"] == False:
if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]}
iso_proc = run(
@ -215,13 +219,12 @@ def download_file_to_iso(scsi_id, url):
def download_to_dir(url, save_dir):
"""
Takes str url, str save_dir
Returns dict with boolean status and str msg
Takes (str) url, (str) save_dir
Returns (dict) with (bool) status and (str) msg
"""
import requests
from pathlib import PurePath
file_name = PurePath(url).name
logging.info(f"Making a request to download {url}")
logging.info("Making a request to download %s", url)
try:
with requests.get(url, stream=True, headers={"User-Agent": "Mozilla/5.0"}) as req:
@ -229,29 +232,28 @@ def download_to_dir(url, save_dir):
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
except requests.exceptions.RequestException as e:
logging.warning(f"Request failed: {str(e)}")
return {"status": False, "msg": str(e)}
except requests.exceptions.RequestException as error:
logging.warning("Request failed: %s", str(error))
return {"status": False, "msg": str(error)}
logging.info("Response encoding: %s", req.encoding)
logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code)
logging.info(f"Response encoding: {req.encoding}")
logging.info(f"Response content-type: {req.headers['content-type']}")
logging.info(f"Response status code: {req.status_code}")
return {"status": True, "msg": f"{url} downloaded to {save_dir}"}
return {"status": True, "msg": f"File downloaded from {url} to {save_dir}"}
def write_config(file_name):
"""
Takes str file_name
Returns dict with boolean status and str msg
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_name = cfg_dir + file_name
file_name = CFG_DIR + file_name
try:
with open(file_name, "w") as json_file:
devices = list_devices()["device_list"]
if len(devices) == 0:
if not devices:
return {"status": False, "msg": "No attached devices."}
for device in devices:
# Remove keys that we don't want to store in the file
@ -269,86 +271,90 @@ def write_config(file_name):
# Convert to a data type that can be serialized
device["params"] = dict(device["params"])
dump(devices, json_file, indent=4)
return {"status": True, "msg": f"Successfully wrote to file: {file_name}"}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": True, "msg": f"Saved config to {file_name}"}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_name)
return {"status": False, "msg": str(e)}
return {"status": False, "msg": str(error)}
except:
logging.error(f"Could not write to file: {file_name}")
logging.error("Could not write to file: %s", file_name)
delete_file(file_name)
return {"status": False, "msg": f"Could not write to file: {file_name}"}
def read_config(file_name):
"""
Takes str file_name
Returns dict with boolean status and str msg
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import load
file_name = cfg_dir + file_name
file_name = CFG_DIR + file_name
try:
with open(file_name) as json_file:
detach_all()
devices = load(json_file)
for row in devices:
kwargs = {"device_type": row["device_type"], \
"image": row["image"], "unit": int(row["un"]), \
"vendor": row["vendor"], "product": row["product"], \
"revision": row["revision"], "block_size": row["block_size"]}
kwargs = {
"device_type": row["device_type"],
"image": row["image"],
"unit": int(row["un"]),
"vendor": row["vendor"],
"product": row["product"],
"revision": row["revision"],
"block_size": row["block_size"],
}
params = dict(row["params"])
for p in params.keys():
kwargs[p] = params[p]
for param in params.keys():
kwargs[param] = params[param]
process = attach_image(row["id"], **kwargs)
if process["status"] == True:
return {"status": process["status"], "msg": f"Successfully read from file: {file_name}"}
else:
return {"status": process["status"], "msg": process["msg"]}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
if process["status"]:
return {"status": process["status"], "msg": f"Loaded config from: {file_name}"}
return {"status": process["status"], "msg": process["msg"]}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error(f"Could not read file: {file_name}")
logging.error("Could not read file: %s", file_name)
return {"status": False, "msg": f"Could not read file: {file_name}"}
def write_drive_properties(file_name, conf):
"""
Writes a drive property configuration file to the config dir.
Takes file name base (str) and conf (list of dicts) as arguments
Returns dict with boolean status and str msg
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 = cfg_dir + file_name
file_path = CFG_DIR + file_name
try:
with open(file_path, "w") as json_file:
dump(conf, json_file, indent=4)
return {"status": True, "msg": f"Successfully wrote to file: {file_path}"}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": True, "msg": f"Created file: {file_path}"}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
delete_file(file_path)
return {"status": False, "msg": str(e)}
return {"status": False, "msg": str(error)}
except:
logging.error(f"Could not write to file: {file_path}")
logging.error("Could not write to file: %s", file_path)
delete_file(file_path)
return {"status": False, "msg": f"Could not write to file: {file_path}"}
def read_drive_properties(path_name):
"""
"""
Reads drive properties from json formatted file.
Takes (str) path_name as argument.
Returns dict with boolean status, str msg, dict conf
Returns (dict) with (bool) status, (str) msg, (dict) conf
"""
from json import load
try:
with open(path_name) as json_file:
conf = load(json_file)
return {"status": True, "msg": f"Read data from file: {path_name}", "conf": conf}
except (IOError, ValueError, EOFError, TypeError) as e:
logging.error(str(e))
return {"status": False, "msg": str(e)}
return {"status": True, "msg": f"Read from file: {path_name}", "conf": conf}
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return {"status": False, "msg": str(error)}
except:
logging.error(f"Could not read file: {file_name}")
logging.error("Could not read file: %s", path_name)
return {"status": False, "msg": f"Could not read file: {path_name}"}

View File

@ -1,8 +1,15 @@
"""
Module for methods controlling and getting information about the Pi's Linux system
"""
import subprocess
def systemd_service(service, action):
# start/stop/restart
"""
Takes (str) service and (str) action
Action can be one of start/stop/restart
"""
return (
subprocess.run(["sudo", "/bin/systemctl", action, service]).returncode
== 0
@ -10,16 +17,27 @@ def systemd_service(service, action):
def reboot_pi():
"""
Reboots the Pi system
"""
subprocess.Popen(["sudo", "reboot"])
return True
def shutdown_pi():
"""
Shuts down the Pi system
"""
subprocess.Popen(["sudo", "shutdown", "-h", "now"])
return True
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
"""
ra_git_version = (
subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True)
.stdout.decode("utf-8")
@ -35,7 +53,7 @@ def running_env():
def running_netatalk():
"""
Returns int afpd, which is the number of afpd processes currently running
Returns (int) afpd, which is the number of afpd processes currently running
"""
process = subprocess.run(["ps", "aux"], capture_output=True)
output = process.stdout.decode("utf-8")
@ -45,6 +63,9 @@ def running_netatalk():
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
process = subprocess.run(["brctl", "show"], capture_output=True)
output = process.stdout.decode("utf-8")
if "rascsi_bridge" in output:
@ -53,6 +74,10 @@ def is_bridge_setup():
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}

View File

@ -1,6 +1,9 @@
import logging
"""
Module for commands sent to the RaSCSI backend service.
"""
from settings import *
from settings import REMOVABLE_DEVICE_TYPES
from socket_cmds import send_pb_command
import rascsi_interface_pb2 as proto
@ -8,12 +11,12 @@ def get_server_info():
"""
Sends a SERVER_INFO command to the server.
Returns a dict with:
- boolean status
- str version (RaSCSI version number)
- list of str log_levels (the log levels RaSCSI supports)
- str current_log_level
- list of int reserved_ids
- 5 distinct lists of strs with file endings recognized by RaSCSI
- (bool) status
- (str) version (RaSCSI version number)
- (list) of (str) log_levels (the log levels RaSCSI supports)
- (str) current_log_level
- (list) of (int) reserved_ids
- 5 distinct (list)s of (str)s with file endings recognized by RaSCSI
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.SERVER_INFO
@ -36,39 +39,39 @@ def get_server_info():
scrm = []
scmo = []
sccd = []
for m in mappings:
if mappings[m] == proto.PbDeviceType.SAHD:
sahd.append(m)
elif mappings[m] == proto.PbDeviceType.SCHD:
schd.append(m)
elif mappings[m] == proto.PbDeviceType.SCRM:
scrm.append(m)
elif mappings[m] == proto.PbDeviceType.SCMO:
scmo.append(m)
elif mappings[m] == proto.PbDeviceType.SCCD:
sccd.append(m)
for dtype in mappings:
if mappings[dtype] == proto.PbDeviceType.SAHD:
sahd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCHD:
schd.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCRM:
scrm.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCMO:
scmo.append(dtype)
elif mappings[dtype] == proto.PbDeviceType.SCCD:
sccd.append(dtype)
return {
"status": result.status,
"version": version,
"log_levels": log_levels,
"current_log_level": current_log_level,
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
"sccd": sccd,
}
"status": result.status,
"version": version,
"log_levels": log_levels,
"current_log_level": current_log_level,
"reserved_ids": reserved_ids,
"image_dir": image_dir,
"sahd": sahd,
"schd": schd,
"scrm": scrm,
"scmo": scmo,
"sccd": sccd,
}
def get_network_info():
"""
Sends a NETWORK_INTERFACES_INFO command to the server.
Returns a dict with:
- boolean status
- list of str ifs (network interfaces detected by RaSCSI)
- (bool) status
- (list) of (str) ifs (network interfaces detected by RaSCSI)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.NETWORK_INTERFACES_INFO
@ -84,8 +87,8 @@ def get_device_types():
"""
Sends a DEVICE_TYPES_INFO command to the server.
Returns a dict with:
- boolean status
- list of str device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
- (bool) status
- (list) of (str) device_types (device types that RaSCSI supports, ex. SCHD, SCCD, etc)
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DEVICE_TYPES_INFO
@ -94,44 +97,20 @@ def get_device_types():
result = proto.PbResult()
result.ParseFromString(data)
device_types = []
for t in result.device_types_info.properties:
device_types.append(proto.PbDeviceType.Name(t.type))
for prop in result.device_types_info.properties:
device_types.append(proto.PbDeviceType.Name(prop.type))
return {"status": result.status, "device_types": device_types}
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of dicts devices, and list of ints reserved_ids.
Returns:
- list of ints 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 d in devices:
occupied_ids.append(d["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 len(unoccupied_ids) > 0:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def attach_image(scsi_id, **kwargs):
"""
Takes int scsi_id and kwargs containing 0 or more device properties
Takes (int) scsi_id and kwargs containing 0 or more device properties
If the current attached device is a removable device wihout media inserted,
this sends a INJECT command to the server.
If there is no currently attached device, this sends the ATTACH command to the server.
Returns boolean status and str msg
Returns (bool) status and (str) msg
"""
command = proto.PbCommand()
@ -149,19 +128,21 @@ def attach_image(scsi_id, **kwargs):
devices.params["file"] = kwargs["image"]
# Handling the inserting of media into an attached removable type device
device_type = kwargs.get("device_type", None)
device_type = kwargs.get("device_type", None)
currently_attached = list_devices(scsi_id, kwargs.get("unit"))["device_list"]
if len(currently_attached) > 0:
current_type = currently_attached[0]["device_type"]
if currently_attached:
current_type = currently_attached[0]["device_type"]
else:
current_type = None
if device_type in REMOVABLE_DEVICE_TYPES and current_type in REMOVABLE_DEVICE_TYPES:
if current_type != device_type:
return {"status": False, "msg": f"Cannot insert an image for \
{device_type} into a {current_type} device."}
else:
command.operation = proto.PbOperation.INSERT
return {
"status": False,
"msg": "Cannot insert an image for " + device_type + \
" into a " + current_type + " device."
}
command.operation = proto.PbOperation.INSERT
# Handling attaching a new device
else:
command.operation = proto.PbOperation.ATTACH
@ -169,13 +150,13 @@ def attach_image(scsi_id, **kwargs):
if kwargs["interfaces"] not in [None, ""]:
devices.params["interfaces"] = kwargs["interfaces"]
if "vendor" in kwargs.keys():
if kwargs["vendor"] != None:
if kwargs["vendor"] is not None:
devices.vendor = kwargs["vendor"]
if "product" in kwargs.keys():
if kwargs["product"] != None:
if kwargs["product"] is not None:
devices.product = kwargs["product"]
if "revision" in kwargs.keys():
if kwargs["revision"] != None:
if kwargs["revision"] is not None:
devices.revision = kwargs["revision"]
if "block_size" in kwargs.keys():
if kwargs["block_size"] not in [None, ""]:
@ -189,16 +170,16 @@ def attach_image(scsi_id, **kwargs):
return {"status": result.status, "msg": result.msg}
def detach_by_id(scsi_id, un=None):
def detach_by_id(scsi_id, unit=None):
"""
Takes int scsi_id and optional int un
Takes (int) scsi_id and optional (int) unit.
Sends a DETACH command to the server.
Returns boolean status and str msg.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if un != None:
devices.unit = int(un)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH
@ -213,7 +194,7 @@ def detach_by_id(scsi_id, un=None):
def detach_all():
"""
Sends a DETACH_ALL command to the server.
Returns boolean status and str msg.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.DETACH_ALL
@ -224,16 +205,16 @@ def detach_all():
return {"status": result.status, "msg": result.msg}
def eject_by_id(scsi_id, un=None):
def eject_by_id(scsi_id, unit=None):
"""
Takes int scsi_id and optional int un.
Takes (int) scsi_id and optional (int) unit.
Sends an EJECT command to the server.
Returns boolean status and str msg.
Returns (bool) status and (str) msg.
"""
devices = proto.PbDeviceDefinition()
devices.id = int(scsi_id)
if un != None:
devices.unit = int(un)
if unit is not None:
devices.unit = int(unit)
command = proto.PbCommand()
command.operation = proto.PbOperation.EJECT
@ -245,14 +226,14 @@ def eject_by_id(scsi_id, un=None):
return {"status": result.status, "msg": result.msg}
def list_devices(scsi_id=None, un=None):
def list_devices(scsi_id=None, unit=None):
"""
Takes optional int scsi_id and optional int un.
Takes optional (int) scsi_id and optional (int) unit.
Sends a DEVICES_INFO command to the server.
If no scsi_id is provided, returns a list of dicts of all attached devices.
If scsi_id is is provided, returns a list of one dict for the given device.
If no attached device is found, returns an empty list.
Returns boolean status, list of dicts device_list
If no scsi_id is provided, returns a (list) of (dict)s of all attached devices.
If scsi_id is is provided, returns a (list) of one (dict) for the given device.
If no attached device is found, returns an empty (list).
Returns (bool) status, (list) of dicts device_list
"""
from os import path
command = proto.PbCommand()
@ -260,11 +241,11 @@ def list_devices(scsi_id=None, un=None):
# If method is called with scsi_id parameter, return the info on those devices
# Otherwise, return the info on all attached devices
if scsi_id != None:
if scsi_id is not None:
device = proto.PbDeviceDefinition()
device.id = int(scsi_id)
if un != None:
device.unit = int(un)
if unit is not None:
device.unit = int(unit)
command.devices.append(device)
data = send_pb_command(command.SerializeToString())
@ -272,89 +253,63 @@ def list_devices(scsi_id=None, un=None):
result.ParseFromString(data)
device_list = []
n = 0
# Return an empty list if no devices are attached
if len(result.devices_info.devices) == 0:
# Return an empty (list) if no devices are attached
if not result.devices_info.devices:
return {"status": False, "device_list": []}
while n < len(result.devices_info.devices):
did = result.devices_info.devices[n].id
dun = result.devices_info.devices[n].unit
dtype = proto.PbDeviceType.Name(result.devices_info.devices[n].type)
dstat = result.devices_info.devices[n].status
dprop = result.devices_info.devices[n].properties
i = 0
while i < len(result.devices_info.devices):
did = result.devices_info.devices[i].id
dunit = result.devices_info.devices[i].unit
dtype = proto.PbDeviceType.Name(result.devices_info.devices[i].type)
dstat = result.devices_info.devices[i].status
dprop = result.devices_info.devices[i].properties
# Building the status string
# TODO: This formatting should probably be moved elsewhere
dstat_msg = []
if dprop.read_only == True:
if dprop.read_only:
dstat_msg.append("Read-Only")
if dstat.protected == True and dprop.protectable == True:
if dstat.protected and dprop.protectable:
dstat_msg.append("Write-Protected")
if dstat.removed == True and dprop.removable == True:
if dstat.removed and dprop.removable:
dstat_msg.append("No Media")
if dstat.locked == True and dprop.lockable == True:
if dstat.locked and dprop.lockable:
dstat_msg.append("Locked")
dpath = result.devices_info.devices[n].file.name
dpath = result.devices_info.devices[i].file.name
dfile = path.basename(dpath)
dparam = result.devices_info.devices[n].params
dven = result.devices_info.devices[n].vendor
dprod = result.devices_info.devices[n].product
drev = result.devices_info.devices[n].revision
dblock = result.devices_info.devices[n].block_size
dsize = int(result.devices_info.devices[n].block_count) * int(dblock)
dparam = result.devices_info.devices[i].params
dven = result.devices_info.devices[i].vendor
dprod = result.devices_info.devices[i].product
drev = result.devices_info.devices[i].revision
dblock = result.devices_info.devices[i].block_size
dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
device_list.append(
{
"id": did,
"un": dun,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"image": dpath,
"file": dfile,
"params": dparam,
"vendor": dven,
"product": dprod,
"revision": drev,
"block_size": dblock,
"size": dsize,
}
)
n += 1
device_list.append({
"id": did,
"un": dunit,
"device_type": dtype,
"status": ", ".join(dstat_msg),
"image": dpath,
"file": dfile,
"params": dparam,
"vendor": dven,
"product": dprod,
"revision": drev,
"block_size": dblock,
"size": dsize,
})
i += 1
return {"status": True, "device_list": device_list}
def sort_and_format_devices(devices):
"""
Takes a list of dicts devices and returns a list of dicts.
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 d in devices:
occupied_ids.append(d["id"])
formatted_devices = devices
# Add padding devices and sort the list
for id in range(8):
if id not in occupied_ids:
formatted_devices.append({"id": id, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
return {"status": result.status, "msg": result.msg, "device_list": device_list}
def set_log_level(log_level):
"""
Sends a LOG_LEVEL command to the server.
Takes str log_level as an argument.
Returns boolean status and str msg.
Takes (str) log_level as an argument.
Returns (bool) status and (str) msg.
"""
command = proto.PbCommand()
command.operation = proto.PbOperation.LOG_LEVEL
@ -364,89 +319,3 @@ def set_log_level(log_level):
result = proto.PbResult()
result.ParseFromString(data)
return {"status": result.status, "msg": result.msg}
def send_pb_command(payload):
"""
Takes a str containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
HOST = 'localhost'
PORT = 6868
counter = 0
tries = 100
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
return send_over_socket(s, payload)
except socket.error as error:
counter += 1
logging.warning("The RaSCSI service is not responding - attempt " + \
str(counter) + "/" + str(tries))
error_msg = str(error)
logging.error(error_msg)
# After failing all attempts, throw a 404 error
from flask import abort
abort(404, "Failed to connect to RaSCSI at " + str(HOST) + ":" + str(PORT) + \
" with error: " + error_msg + ". Is the RaSCSI service running?")
def send_over_socket(s, payload):
"""
Takes a socket object and str payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
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
s.send(b"RASCSI")
# Prepending a little endian 32bit header with the message size
s.send(pack("<i", len(payload)))
s.send(payload)
# Receive the first 4 bytes to get the response header
response = s.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = s.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
from flask import abort
logging.error(
"Read an empty chunk from the socket. "
"Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
abort(503, "Lost connection to RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
else:
from flask import abort
logging.error(
"The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
abort(500, "Did not get a valid response from RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)

View File

@ -3,12 +3,6 @@ click==7.1.2
Flask==2.0.1
itsdangerous==2.0.1
Jinja2==3.0.1
machfs==1.2.4
macresources==1.2
MarkupSafe==2.0.1
rsrcfork==1.8.0
waitress==1.4.4
zope.event==4.5.0
zope.interface==5.1.2
protobuf==3.17.3
pydrop==0.0.6
requests==2.26.0

View File

@ -1,20 +1,26 @@
"""
Constant definitions used by other modules
"""
from os import getenv, getcwd
# TODO: Make home_dir portable when running rascsi-web
# TODO: Make HOME_DIR portable when running rascsi-web
# as a service, since the HOME env variable doesn't get set then.
home_dir = getenv("HOME", "/home/pi")
cfg_dir = f"{home_dir}/.config/rascsi/"
afp_dir = f"{home_dir}/afpshare"
web_dir = getcwd()
HOME_DIR = getenv("HOME", "/home/pi")
CFG_DIR = f"{HOME_DIR}/.config/rascsi/"
AFP_DIR = f"{HOME_DIR}/afpshare"
WEB_DIR = getcwd()
DEFAULT_CONFIG = "default.json"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", 1024 * 1024 * 1024 * 4) # 4gb
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb
ARCHIVE_FILE_SUFFIX = ("zip",)
# File containing canonical drive properties
DRIVE_PROPERTIES_FILE = web_dir + "/drive_properties.json"
ARCHIVE_FILE_SUFFIX = "zip"
CONFIG_FILE_SUFFIX = "json"
# File ending used for drive properties files
PROPERTIES_SUFFIX = "properties"
# The file name of the default config file that loads when rascsi-web starts
DEFAULT_CONFIG = f"default.{CONFIG_FILE_SUFFIX}"
# File containing canonical drive properties
DRIVE_PROPERTIES_FILE = WEB_DIR + "/drive_properties.json"
REMOVABLE_DEVICE_TYPES = ("SCCD", "SCRM", "SCMO")

92
src/web/socket_cmds.py Normal file
View File

@ -0,0 +1,92 @@
"""
Module for sending and receiving data over a socket connection with the RaSCSI backend
"""
import logging
def send_pb_command(payload):
"""
Takes a (str) containing a serialized protobuf as argument.
Establishes a socket connection with RaSCSI.
"""
# Host and port number where rascsi is listening for socket connections
host = 'localhost'
port = 6868
counter = 0
tries = 100
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.connect((host, port))
return send_over_socket(sock, payload)
except socket.error as error:
counter += 1
logging.warning("The RaSCSI service is not responding - attempt %s/%s",
str(counter), str(tries))
error_msg = str(error)
logging.error(error_msg)
# After failing all attempts, throw a 404 error
from flask import abort
abort(404, "Failed to connect to RaSCSI at " + str(host) + ":" + str(port) + \
" with error: " + error_msg + ". Is the RaSCSI service running?")
def send_over_socket(sock, payload):
"""
Takes a socket object and (str) payload with serialized protobuf.
Sends payload to RaSCSI over socket and captures the response.
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")
# Prepending a little endian 32bit header with the message size
sock.send(pack("<i", len(payload)))
sock.send(payload)
# Receive the first 4 bytes to get the response header
response = sock.recv(4)
if len(response) >= 4:
# Extracting the response header to get the length of the response message
response_length = unpack("<i", response)[0]
# Reading in chunks, to handle a case where the response message is very large
chunks = []
bytes_recvd = 0
while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'':
from flask import abort
logging.error(
"Read an empty chunk from the socket. "
"Socket connection has dropped unexpectedly. "
"RaSCSI may have crashed."
)
abort(
503, "Lost connection to RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)
chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks)
return response_message
from flask import abort
logging.error(
"The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
abort(
500,
"Did not get a valid response from RaSCSI. "
"Please go back and try again. "
"If the issue persists, please report a bug."
)

View File

@ -26,33 +26,33 @@ if ! command -v unzip &> /dev/null ; then
ERROR=1
fi
if [ $ERROR = 1 ] ; then
echo
echo "Fix errors and re-run ./start.sh"
exit 1
echo
echo "Fix errors and re-run ./start.sh"
exit 1
fi
if ! test -e venv; then
echo "Creating python venv for web server"
python3 -m venv venv
echo "Activating venv"
source venv/bin/activate
echo "Installing requirements.txt"
pip install wheel
pip install -r requirements.txt
git rev-parse HEAD > current
echo "Creating python venv for web server"
python3 -m venv venv
echo "Activating venv"
source venv/bin/activate
echo "Installing requirements.txt"
pip install wheel
pip install -r requirements.txt
git rev-parse HEAD > current
fi
source venv/bin/activate
# Detect if someone updates - we need to re-run pip install.
if ! test -e current; then
git rev-parse > current
git rev-parse > current
else
if [ "$(cat current)" != "$(git rev-parse HEAD)" ]; then
echo "New version detected, updating requirements.txt"
pip install -r requirements.txt
git rev-parse HEAD > current
fi
if [ "$(cat current)" != "$(git rev-parse HEAD)" ]; then
echo "New version detected, updating requirements.txt"
pip install -r requirements.txt
git rev-parse HEAD > current
fi
fi
echo "Starting web server..."

View File

@ -1,4 +1,6 @@
<!doctype html>
<html>
<head>
<title>RaSCSI Control Page</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
@ -24,7 +26,9 @@
<script type="application/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js">
</script>
</head>
<body>
<div class="content">
<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>
@ -43,17 +47,18 @@
<div class="flash">
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "stdout" or category == "stderr" %}
<pre>{{message}}</pre>
<pre>{{ message }}</pre>
{% else %}
<div class="{{category}}">{{ message }}</div>
<div class="{{ category }}">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
<div class="content">
{% block content %}{% endblock %}
{% block content %}{% endblock content %}
</div>
<div class="footer">
<center><tt>RaSCSI version: <strong>{{version}} <a href="https://github.com/akuker/RASCSI/commit/{{running_env["git"]}}">{{running_env["git"][:7]}}</a></strong></tt></center>
<center><tt>Pi environment: {{running_env["env"]}}</tt></center>
<center><tt>RaSCSI version: <strong>{{ version }} <a href="https://github.com/akuker/RASCSI/commit/{{ running_env['git'] }}">{{ running_env["git"][:7] }}</a></strong></tt></center>
<center><tt>Pi environment: {{ running_env["env"] }}</tt></center>
</div>
</div>
</body>

View File

@ -1,141 +1,141 @@
{% extends "base.html" %}
{% block content %}
<p><a href="/">Cancel</a></p>
<h2>Disclaimer</h2>
<p>These device profiles are provided as-is with no guarantee to work on the systems mentioned. You may need appropirate device drivers and/or configuration parameters. If you have improvement suggestions or success stories to share we would love to hear from you, so please connect with us at <a href="https://github.com/akuker/RASCSI">GitHub</a> or <a href="https://discord.gg/PyS58u6">Discord</a>!</p>
<h2>Hard Drives</h2>
{% block content %}
<p><a href="/">Cancel</a></p>
<h2>Disclaimer</h2>
<p>These device profiles are provided as-is with no guarantee to work on the systems mentioned. You may need appropirate device drivers and/or configuration parameters. If you have improvement suggestions or success stories to share we would love to hear from you, so please connect with us at <a href="https://github.com/akuker/RASCSI">GitHub</a> or <a href="https://discord.gg/PyS58u6">Discord</a>!</p>
<h2>Hard Drives</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for hd in hd_conf %}
<tr>
<td style="text-align:center">{{hd.name}}</td>
<td style="text-align:center">{{hd.size_mb}}</td>
<td style="text-align:left">{{hd.description}}</td>
<td style="text-align:left">
{% if hd.url != "" %}
<a href="{{hd.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{hd.vendor}}">
<input type="hidden" name="product" value="{{hd.product}}">
<input type="hidden" name="revision" value="{{hd.revision}}">
<input type="hidden" name="blocks" value="{{hd.blocks}}">
<input type="hidden" name="block_size" value="{{hd.block_size}}">
<input type="hidden" name="size" value="{{hd.size}}">
<input type="hidden" name="file_type" value="{{hd.file_type}}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{hd.secure_name}}" required />.{{hd.file_type}}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for hd in hd_conf %}
<tr>
<td style="text-align:center">{{ hd.name }}</td>
<td style="text-align:center">{{ hd.size_mb }}</td>
<td style="text-align:left">{{ hd.description }}</td>
<td style="text-align:left">
{% if hd.url != "" %}
<a href="{{ hd.url }}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{ hd.vendor }}">
<input type="hidden" name="product" value="{{ hd.product }}">
<input type="hidden" name="revision" value="{{ hd.revision }}">
<input type="hidden" name="blocks" value="{{ hd.blocks }}">
<input type="hidden" name="block_size" value="{{ hd.block_size }}">
<input type="hidden" name="size" value="{{ hd.size }}">
<input type="hidden" name="file_type" value="{{ hd.file_type }}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{ hd.secure_name }}" required />.{{ hd.file_type }}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr/>
<hr/>
<h2>CD-ROM Drives</h2>
<p><em>This will create a properties file for the given CD-ROM image. No new image file will be created.</em></p>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for cd in cd_conf %}
<tr>
<td style="text-align:center">{{cd.name}}</td>
<td style="text-align:center">{{cd.size_mb}}</td>
<td style="text-align:left">{{cd.description}}</td>
<td style="text-align:left">
{% if cd.url != "" %}
<a href="{{cd.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/cdrom" method="post">
<input type="hidden" name="vendor" value="{{cd.vendor}}">
<input type="hidden" name="product" value="{{cd.product}}">
<input type="hidden" name="revision" value="{{cd.revision}}">
<input type="hidden" name="block_size" value="{{cd.block_size}}">
<label for="file_name">Create for:</label>
<select type="select" name="file_name">
{% for f in files %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{f["name"]}}">{{f["name"].replace(base_dir, '')}}</option>
{% endif %}
{% endfor %}
</select>
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<h2>CD-ROM Drives</h2>
<p><em>This will create a properties file for the given CD-ROM image. No new image file will be created.</em></p>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for cd in cd_conf %}
<tr>
<td style="text-align:center">{{ cd.name }}</td>
<td style="text-align:center">{{ cd.size_mb }}</td>
<td style="text-align:left">{{ cd.description }}</td>
<td style="text-align:left">
{% if cd.url != "" %}
<a href="{{ cd.url }}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/cdrom" method="post">
<input type="hidden" name="vendor" value="{{ cd.vendor }}">
<input type="hidden" name="product" value="{{ cd.product }}">
<input type="hidden" name="revision" value="{{ cd.revision }}">
<input type="hidden" name="block_size" value="{{ cd.block_size }}">
<label for="file_name">Create for:</label>
<select type="select" name="file_name">
{% for f in files %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
{% endif %}
{% endfor %}
</select>
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<hr/>
<hr/>
<h2>Removable Drives</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for rm in rm_conf %}
<tr>
<td style="text-align:center">{{rm.name}}</td>
<td style="text-align:center">{{rm.size_mb}}</td>
<td style="text-align:left">{{rm.description}}</td>
<td style="text-align:left">
{% if rm.url != "" %}
<a href="{{rm.url}}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{rm.vendor}}">
<input type="hidden" name="product" value="{{rm.product}}">
<input type="hidden" name="revision" value="{{rm.revision}}">
<input type="hidden" name="blocks" value="{{rm.blocks}}">
<input type="hidden" name="block_size" value="{{rm.block_size}}">
<input type="hidden" name="size" value="{{rm.size}}">
<input type="hidden" name="file_type" value="{{rm.file_type}}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{rm.secure_name}}" required />.{{rm.file_type}}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>Available disk space on the Pi: {{free_disk}} MB</small></p>
<p><a href="/">Cancel</a></p>
<h2>Removable Drives</h2>
<table cellpadding="3" border="black">
<tbody>
<tr>
<td><b>Name</b></td>
<td><b>Size (MB)</b></td>
<td><b>Description</b></td>
<td><b>Ref.</b></td>
<td><b>Action</b></td>
</tr>
{% for rm in rm_conf %}
<tr>
<td style="text-align:center">{{ rm.name }}</td>
<td style="text-align:center">{{ rm.size_mb }}</td>
<td style="text-align:left">{{ rm.description }}</td>
<td style="text-align:left">
{% if rm.url != "" %}
<a href="{{ rm.url }}">Link</a>
{% else %}
-
{% endif %}
</td>
<td style="text-align:left">
<form action="/drive/create" method="post">
<input type="hidden" name="vendor" value="{{ rm.vendor }}">
<input type="hidden" name="product" value="{{ rm.product }}">
<input type="hidden" name="revision" value="{{ rm.revision }}">
<input type="hidden" name="blocks" value="{{ rm.blocks }}">
<input type="hidden" name="block_size" value="{{ rm.block_size }}">
<input type="hidden" name="size" value="{{ rm.size }}">
<input type="hidden" name="file_type" value="{{ rm.file_type }}">
<label for="file_name">Save as:</label>
<input type="text" name="file_name" value="{{ rm.secure_name }}" required />.{{ rm.file_type }}
<input type="submit" value="Create" />
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>Available disk space on the Pi: {{ free_disk }} MB</small></p>
<p><a href="/">Cancel</a></p>
{% endblock %}
{% endblock content %}

View File

@ -1,44 +1,47 @@
{% extends "base.html" %}
{% block content %}
{% block content %}
<details>
<summary class="heading">Current RaSCSI Configuration</summary>
<details>
<summary class="heading">
Current RaSCSI Configuration
</summary>
<ul>
<li>Displays the currently attached devices for each available SCSI ID.</li>
<li>Save and load device configurations into <tt>{{cfg_dir}}</tt></li>
<li>The <em>default</em> configuration will be loaded when the Web UI starts up, if available.</li>
<li>Displays the currently attached devices for each available SCSI ID.</li>
<li>Save and load device configurations into <tt>{{ CFG_DIR }}</tt></li>
<li>The <em>default</em> configuration will be loaded when the Web UI starts up, if available.</li>
</ul>
</details>
</details>
<p>
<form action="/config/load" method="post">
<select name="name" width="14" required>
{% if config_files %}
{% for config in config_files %}
<option value="{{config}}">{{config.replace(".json", '')}}</option>
{% endfor %}
{% else %}
<option disabled>No saved configs</option>
{% endif %}
</select>
<input type="submit" name="load" value="Load" onclick="return confirm('Detach all current device and Load config?')" />
<input type="submit" name="delete" value="Delete" onclick="return confirm('Delete config file?')" />
</form>
</p>
<p>
<form action="/config/save" method="post">
<input name="name" placeholder="default" size="20">
<input type="submit" value="Save" />
</form>
</p>
<p><form action="/config/load" method="post">
<select name="name" required="" width="14">
{% if config_files %}
{% for config in config_files %}
<option value="{{ config }}">
{{ config.replace(".json", '') }}
</option>
{% endfor %}
{% else %}
<option disabled>
No saved configs
</option>
{% endif %}
</select>
<input name="load" type="submit" value="Load" onclick="return confirm('Detach all current device and Load config?')">
<input name="delete" type="submit" value="Delete" onclick="return confirm('Delete config file?')">
</form></p>
<table cellpadding="3" border="black">
<tbody>
<p><form action="/config/save" method="post">
<input name="name" placeholder="default" size="20">
<input type="submit" value="Save">
</form></p>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>ID</b></td>
{% if luns %}
{% if units %}
<td><b>LUN</b></td>
{% endif %}
{% endif %}
<td><b>Type</b></td>
<td><b>Status</b></td>
<td><b>File</b></td>
@ -48,50 +51,45 @@
{% for device in devices %}
<tr>
{% if device["id"] not in reserved_scsi_ids %}
<td style="text-align:center">{{device.id}}</td>
{% if luns %}
<td style="text-align:center">{{device.un}}</td>
{% endif %}
<td style="text-align:center">{{device.device_type}}</td>
<td style="text-align:center">{{device.status}}</td>
<td style="text-align:left">{{device.file}}</td>
{% if device.vendor == "RaSCSI" %}
<td style="text-align:center">{{device.product}}</td>
{% else %}
<td style="text-align:center">{{device.vendor}} {{device.product}}</td>
{% endif %}
<td style="text-align:center">{{ device.id }}</td>
{% if units %}
<td style="text-align:center">{{ device.un }}</td>
{% endif %}
<td style="text-align:center">{{ device.device_type }}</td>
<td style="text-align:center">{{ device.status }}</td>
<td style="text-align:left">{{ device.file }}</td>
{% if device.vendor == "RaSCSI" %}
<td style="text-align:center">{{ device.product }}</td>
{% else %}
<td style="text-align:center">{{ device.vendor }} {{ device.product }}</td>
{% endif %}
<td style="text-align:left">
{% if device.device_type != "-" %}
{% if device.device_type in removable_device_types and "No Media" not in device.status %}
{% if device.device_type != "-" %}
{% if device.device_type in REMOVABLE_DEVICE_TYPES and "No Media" not in device.status %}
<form action="/scsi/eject" method="post" onsubmit="return confirm('Eject Disk?')">
<input type="hidden" name="scsi_id" value="{{device.id}}">
<input type="hidden" name="un" value="{{device.un}}">
<input type="submit" value="Eject" />
</form>
<form action="/scsi/info" method="post">
<input type="hidden" name="scsi_id" value="{{device.id}}">
<input type="hidden" name="un" value="{{device.un}}">
<input type="submit" value="Info" />
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.un }}">
<input type="submit" value="Eject">
</form>
{% else %}
<form action="/scsi/detach" method="post" onsubmit="return confirm('Detach Disk?')">
<input type="hidden" name="scsi_id" value="{{device.id}}">
<input type="hidden" name="un" value="{{device.un}}">
<input type="submit" value="Detach" />
<form action="/scsi/detach" method="post" onsubmit="return confirm('Detach Device?')">
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.un }}">
<input type="submit" value="Detach">
</form>
{% endif %}
<form action="/scsi/info" method="post">
<input type="hidden" name="scsi_id" value="{{device.id}}">
<input type="hidden" name="un" value="{{device.un}}">
<input type="submit" value="Info" />
<input name="scsi_id" type="hidden" value="{{ device.id }}">
<input name="unit" type="hidden" value="{{ device.un }}">
<input type="submit" value="Info">
</form>
{% endif %}
{% endif %}
{% endif %}
</td>
{% else %}
<td class="inactive">{{device.id}}</td>
{% if luns %}
<td class="inactive">{{ device.id }}</td>
{% if units %}
<td class="inactive"></td>
{% endif %}
{% endif %}
<td class="inactive"></td>
<td class="inactive">Reserved ID</td>
<td class="inactive"></td>
@ -100,28 +98,30 @@
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<p>
<form action="/scsi/detach_all" method="post" onsubmit="return confirm('Detach all SCSI Devices?')">
<input type="submit" value="Detach All Devices" />
</form>
</p>
</tbody>
</table>
<hr/>
<p><form action="/scsi/detach_all" method="post" onsubmit="return confirm('Detach all SCSI Devices?')">
<input type="submit" value="Detach All Devices">
</form></p>
<details>
<summary class="heading">Image File Management</summary>
<hr/>
<details>
<summary class="heading">
Image File Management
</summary>
<ul>
<li>Manage image files in the active RaSCSI image directory: <tt>{{base_dir}}</tt></li>
<li>Select a valid SCSI ID and <a href="https://en.wikipedia.org/wiki/Logical_unit_number">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.</li>
<li>If RaSCSI was unable to detect the device type associated with the image, you can choose the type from the dropdown.</li>
<li>Types: SAHD = SASI HDD | SCHD = SCSI HDD | SCRM = Removable | SCMO = Magneto-Optical | SCCD = CD-ROM | SCBR = Host Bridge | SCDP = DaynaPORT</li>
<li>Manage image files in the active RaSCSI image directory: <tt>{{ base_dir }}</tt></li>
<li>Select a valid SCSI ID and <a href="https://en.wikipedia.org/wiki/Logical_unit_number">LUN</a> to attach to. Unless you know what you're doing, always use LUN 0.
</li>
<li>If RaSCSI was unable to detect the device type associated with the image, you can choose the type from the dropdown.</li>
<li>Types: SAHD = SASI HDD | SCHD = SCSI HDD | SCRM = Removable | SCMO = Magneto-Optical | SCCD = CD-ROM | SCBR = Host Bridge | SCDP = DaynaPORT</li>
</ul>
</details>
</details>
<table cellpadding="3" border="black">
<tbody>
<table border="black" cellpadding="3">
<tbody>
<tr>
<td><b>File</b></td>
<td><b>Size</b></td>
@ -132,365 +132,420 @@
{% if file["prop"] %}
<td>
<details>
<summary>{{file["name"]}}</summary>
<summary>
{{ file["name"] }}
</summary>
<ul>
{% for key in file["prop"] %}
<li>{{key}}: {{file['prop'][key]}}</li>
<li>{{ key }}: {{ file['prop'][key] }}</li>
{% endfor %}
</ul>
</details>
</details>
</td>
{% elif file["zip"] %}
{% elif file["zip"] %}
<td>
<details>
<summary>{{file["name"]}}</summary>
<summary>
{{ file["name"] }}
</summary>
<ul>
{% for member in file["zip"] %}
<li>
<label for="member">{{member}}</label>
<form action="/files/unzip" method="post">
<input type="hidden" name="image" value="{{file['name']}}">
<input type="hidden" name="member" value="{{member}}">
<input type="submit" value="Unzip" />
</form>
</li>
<li>
<label for="member">{{ member }}</label>
<form action="/files/unzip" method="post">
<input name="image" type="hidden" value="{{ file['name'] }}">
<input name="member" type="hidden" value="{{ member }}">
<input type="submit" value="Unzip">
</form>
</li>
{% endfor %}
</ul>
</details>
</details>
</td>
{% else %}
<td>{{file["name"]}}</td>
{% endif %}
{% else %}
<td>{{ file["name"] }}</td>
{% endif %}
<td style="text-align:center">
<form action="/files/download" method="post">
<input type="hidden" name="image" value="{{file["name"]}}">
<input type="submit" value="{{file["size_mb"]}} MB &#8595;" />
<input name="image" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="{{ file["size_mb"] }} MB &#8595;">
</form>
</td>
<td>
{% if file["name"] in attached_images %}
<center>Attached!</center>
{% else %}
{% if file["name"].lower().endswith(archive_file_suffix) %}
<form action="/files/unzip" method="post">
<input type="hidden" name="image" value="{{file["name"]}}">
<input type="submit" value="Unzip All" />
</form>
{% else %}
<form action="/scsi/attach" method="post">
<input type="hidden" name="file_name" value="{{file["name"]}}">
<input type="hidden" name="file_size" value="{{file["size"]}}">
<label for="id">ID</label>
<td>
{% if file["name"] in attached_images %}
<center>
Attached!
</center>
{% else %}
{% if file["name"].lower().endswith(archive_file_suffix) %}
<form action="/files/unzip" method="post">
<input name="image" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="Unzip All">
</form>
{% else %}
<form action="/scsi/attach" method="post">
<input name="file_name" type="hidden" value="{{ file['name'] }}">
<input name="file_size" type="hidden" value="{{ file['size'] }}">
<label for="id">ID</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>{{id}}</option>
<option name="id" value="{{id}}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="un">LUN</label>
<input type="number" name="un" size="2" value="0" min="0" max="31" />
{% if file["detected_type"] != "UNDEFINED" %}
<input type="hidden" name="type" value="{{file["detected_type"]}}">
{{file["detected_type"]}}
{% else %}
<select name="type">
<option value="" selected>Type</option>
{% for d in device_types %}
<option value="{{d}}">{{d}}</option>
{% endfor %}
{% endif %}
</select>
<input type="submit" value="Attach" />
{% endif %}
<label for="unit">LUN</label>
<input name="unit" type="number" size="2" value="0" min="0" max="31">
{% if file["detected_type"] != "UNDEFINED" %}
<input name="type" type="hidden" value="{{ file['detected_type'] }}">
{{ file["detected_type"] }}
{% else %}
<select name="type">
<option selected value="">
Type
</option>
{% for d in device_types %}
<option value="{{ d }}">
{{ d }}
</option>
{% endfor %}
{% endif %}
</select>
<input type="submit" value="Attach">
{% endif %}
</form>
<form action="/files/delete" method="post" onsubmit="return confirm('Delete file?')">
<input type="hidden" name="image" value="{{file['name']}}">
<input type="submit" value="Delete" />
<input name="image" type="hidden" value="{{ file['name'] }}">
<input type="submit" value="Delete">
</form>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><small>Available disk space on the Pi: {{free_disk}} MB</small></p>
</tbody>
</table>
<p><small>Available disk space on the Pi: {{ free_disk }} MB</small></p>
<hr/>
<hr/>
<details>
<summary class="heading">Attach Ethernet Adapter</summary>
<details>
<summary class="heading">
Attach Ethernet Adapter
</summary>
<ul>
<li>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers and configuration required</a>.</li>
<li>If you have a DHCP setup, choose only the interface, and ignore the Static IP fields when attaching.</li>
<li>Configure network forwarding by running easyinstall.sh, or follow the <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup">manual steps in the wiki</a>.</li>
{% if bridge_configured %}
<li>The <tt>rascsi_bridge</tt> interface is active and ready to be used by DaynaPORT!</li>
{% endif %}
<li>Emulates a SCSI DaynaPORT Ethernet Adapter. <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#-macintosh-setup-instructions">Host drivers and configuration required</a>.
</li>
<li>If you have a DHCP setup, choose only the interface, and ignore the Static IP fields when attaching.</li>
<li>Configure network forwarding by running easyinstall.sh, or follow the <a href="https://github.com/akuker/RASCSI/wiki/Dayna-Port-SCSI-Link#manual-setup">manual steps in the wiki</a>.
</li>
<li style="list-style: none">{% if bridge_configured %}</li>
<li>The <tt>rascsi_bridge</tt> interface is active and ready to be used by DaynaPORT!</li>
<li style="list-style: none">{% endif %}</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/daynaport/attach" method="post">
<label for="if">Interface:</label>
<select name = "if">
{% for if in netinfo["ifs"] %}
<option value="{{if}}">{{if}}</option>
{% endfor %}
</select>
<label for="ip">Static IP (optional):</label>
<input type="text" name="ip" size="15" placeholder="10.10.20.1" minlength="7" maxlength="15" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" />
<input type="number" name="mask" size="2" placeholder="24" min="16" max="30" />
<label for="scsi_id">SCSI ID:</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{id}}"{% if id == recommended_id %} selected{% endif %}>{{id}}</option>
{% endfor %}
</select>
<input type="submit" value="Attach" />
</form>
</td>
</tr>
</table>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/daynaport/attach" method="post">
<label for="if">Interface:</label>
<select name="if">
{% for if in netinfo["ifs"] %}
<option value="{{ if }}">
{{ if }}
</option>
{% endfor %}
</select>
<label for="ip">Static IP (optional):</label>
<input name="ip" type="text" size="15" placeholder="10.10.20.1" minlength="7" maxlength="15" pattern="^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$">
<input name="mask" type="number" size="2" placeholder="24" min="16" max="30">
<label for="scsi_id">SCSI ID:</label>
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<input type="submit" value="Attach">
</form>
</td>
</tr>
</table>
<hr/>
<details>
<summary class="heading">Upload File</summary>
<hr/>
<details>
<summary class="heading">
Upload File
</summary>
<ul>
<li>Uploads file to <tt>{{base_dir}}</tt>. The largest file size accepted is {{max_file_size}} MB.</li>
<li>For unrecognized file types, try renaming hard drive images to '.hds' and CD-ROM images to '.iso' before uploading.</li>
<li>Recognized file types: {{valid_file_suffix}}</li>
<li>Uploads file to <tt>{{ base_dir }}</tt>. The largest file size accepted is {{ max_file_size }} MB.</li>
<li>For unrecognized file types, try renaming hard drive images to '.hds' and CD-ROM images to '.iso' before uploading.</li>
<li>Recognized file types: {{ valid_file_suffix }}</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form method="POST" action="/files/upload" class="dropzone dz-clickable" id="dropper" enctype="multipart/form-data">
</form>
</td>
</tr>
</table>
<script type="application/javascript">
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form name="dropper" action="/files/upload" method="post" class="dropzone dz-clickable" enctype="multipart/form-data" id="dropper"></form>
</td>
</tr>
</table>
<script type="application/javascript">
Dropzone.options.dropper = {
paramName: 'file',
acceptedFiles: '{{valid_file_suffix}}',
acceptedFiles: '{{ valid_file_suffix }}',
chunking: true,
forceChunking: true,
url: '/files/upload',
maxFilesize: {{max_file_size}}, // MB
maxFilesize: {{ max_file_size }}, // MB
chunkSize: 1000000 // bytes
}
</script>
</script>
<hr/>
<hr/>
<details>
<summary class="heading">Download File to Images</summary>
<details>
<summary class="heading">
Download File to Images
</summary>
<ul>
<li>Given a URL, download that file to the <tt>{{base_dir}}</tt> directory.</li>
<li>Given a URL, download that file to the <tt>{{ base_dir }}</tt> directory.</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post">
<label for="url">URL:</label>
<input type="url" placeholder="URL" name="url" required />
<input type="submit" value="Download" />
</form>
</td>
</tr>
</table>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_images" method="post">
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<input type="submit" value="Download">
</form>
</td>
</tr>
</table>
<hr/>
<hr/>
<details>
<summary class="heading">Download File to AppleShare</summary>
<details>
<summary class="heading">
Download File to AppleShare
</summary>
<ul>
<li>Given a URL, download that file to the <tt>{{afp_dir}}</tt> directory and share it over AFP.</li>
<li>Manage the files you download here through AppleShare on your vintage Mac.</li>
<li>Requires <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to be installed and configured correctly for your network.</li>
<li>Given a URL, download that file to the <tt>{{ AFP_DIR }}</tt> directory and share it over AFP.</li>
<li>Manage the files you download here through AppleShare on your vintage Mac.</li>
<li>Requires <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to be installed and configured correctly for your network.
</li>
</ul>
</details>
{% if netatalk_configured %}
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post">
<label for="url">URL:</label>
<input type="url" placeholder="URL" name="url" required />
<input type="submit" value="Download" />
</form>
</td>
</tr>
</table>
{% if netatalk_configured == 1 %}
<p><small>The AppleShare server is running. No active connections</small></p>
{% elif netatalk_configured == 2 %}
<p><small>{{netatalk_configured - 1}} active AFP connection</small></p>
{% elif netatalk_configured > 2 %}
<p><small>{{netatalk_configured - 1}} active AFP connections</small></p>
{% endif %}
{% else %}
<p>Install <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to use the AppleTalk File Server.
{% endif %}
</details>
<hr/>
{% if netatalk_configured %}
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/download_to_afp" method="post">
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<input type="submit" value="Download">
</form>
</td>
</tr>
</table>
<details>
<summary class="heading">Download File from Web and Create HFS CD (Macintosh)</summary>
{% if netatalk_configured == 1 %}
<p><small>The AppleShare server is running. No active connections</small></p>
{% elif netatalk_configured == 2 %}
<p><small>{{ netatalk_configured - 1 }} active AFP connection</small></p>
{% elif netatalk_configured > 2 %}
<p><small>{{ netatalk_configured - 1 }} active AFP connections</small></p>
{% endif %}
{% else %}
<p>Install <a href="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing">Netatalk</a> to use the AppleShare File Server.</p>
{% endif %}
<hr/>
<details>
<summary class="heading">
Download File and Create HFS CD (Macintosh)
</summary>
<ul>
<li>Given a URL this will download a file, create a HFS iso, and mount it on the SCSI ID given.</li>
<li>Requires a <a href="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images">compatible CD-ROM driver</a> installed on the target system.</li>
<li>Given a URL this will download a file, create a HFS iso, and mount it on the SCSI ID given.</li>
<li>Requires a <a href="https://github.com/akuker/RASCSI/wiki/Drive-Setup#Mounting_CD_ISO_or_MO_images">compatible CD-ROM driver</a> installed on the target system.
</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<label for="scsi_id">SCSI ID:</label>
<form action="/files/download_to_iso" method="post">
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{ id }}"{% if id == recommended_id %} selected{% endif %}>
{{ id }}
</option>
{% endfor %}
</select>
<label for="url">URL:</label>
<input name="url" placeholder="URL" required="" type="url">
<input type="submit" value="Download and Mount ISO">
</form>
</td>
</tr>
</table>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<label for="scsi_id">SCSI ID:</label>
<form action="/files/download_to_iso" method="post">
<select name="scsi_id">
{% for id in scsi_ids %}
<option value="{{id}}"{% if id == recommended_id %} selected{% endif %}>{{id}}</option>
{% endfor %}
</select>
<label for="url">URL:</label>
<input type="url" placeholder="URL" name="url" required />
<input type="submit" value="Download and Mount ISO" />
</form>
</td>
</tr>
</table>
<hr/>
<hr/>
<details>
<summary class="heading">Create Empty Disk Image File</summary>
<details>
<summary class="heading">
Create Empty Disk Image File
</summary>
<ul>
<li>The Generic image type is recommended for most systems</li>
<li>APPLE GENUINE and NEC GENUINE image types will make RaSCSI masquerade as a particular drive type that are recognized by Mac and PC98 systems, respectively.</li>
<li>SASI images should only be used on early X68000 or UNIX workstation systems that use this pre-SCSI standard.</li>
<li>The Generic image type is recommended for most systems</li>
<li>APPLE GENUINE and NEC GENUINE image types will make RaSCSI masquerade as a particular drive type that are recognized by Mac and PC98 systems, respectively.</li>
<li>SASI images should only be used on early X68000 or UNIX workstation systems that use this pre-SCSI standard.</li>
</ul>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/create" method="post">
<label for="file_name">File Name:</label>
<input type="text" placeholder="File name" name="file_name" required />
<label for="type">Type:</label>
<select name="type">
<option value="hds">SCSI Hard Disk image (Generic) [.hds]</option>
<option value="hda">SCSI Hard Disk image (APPLE GENUINE - use with Mac) [.hda]</option>
<option value="hdn">SCSI Hard Disk image (NEC GENUINE - use with PC98) [.hdn]</option>
<option value="hdr">SCSI Removable Media Disk image (Generic) [.hdr]</option>
<option value="hdf">SASI Hard Disk image (use with X68000) [.hdf]</option>
</select>
<label for="size">Size:</label>
<input type="number" placeholder="MB" name="size" min="1" size="6" required />
<input type="submit" value="Create" />
</form>
</td>
</tr>
</table>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/files/create" method="post">
<label for="file_name">File Name:</label>
<input name="file_name" placeholder="File name" required="" type="text">
<label for="type">Type:</label>
<select name="type">
<option value="hds">
SCSI Hard Disk image (Generic) [.hds]
</option>
<option value="hda">
SCSI Hard Disk image (APPLE GENUINE - use with Mac) [.hda]
</option>
<option value="hdn">
SCSI Hard Disk image (NEC GENUINE - use with PC98) [.hdn]
</option>
<option value="hdr">
SCSI Removable Media Disk image (Generic) [.hdr]
</option>
<option value="hdf">
SASI Hard Disk image (use with X68000) [.hdf]
</option>
</select>
<label for="size">Size:</label>
<input name="size" type="number" placeholder="MB" min="1" size="6" required>
<input type="submit" value="Create">
</form>
</td>
</tr>
</table>
<hr/>
<hr/>
<details>
<summary class="heading">Create Named Drive</summary>
<details>
<summary class="heading">
Create Named Drive
</summary>
<ul>
<li>Here you can create pairs of images and properties files from a list of real-life drives.</li>
<li>This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems</li>
<li>Here you can create pairs of images and properties files from a list of real-life drives.</li>
<li>This will make RaSCSI use certain vendor strings and block sizes that may improve compatibility with certain systems</li>
</ul>
</details>
</details>
<p><a href="/drive/list">Create a named disk image that mimics real-life drives</a></p>
<p><a href="/drive/list">Create a named disk image that mimics real-life drives</a></p>
<hr/>
<hr/>
<details>
<summary class="heading">Logging</summary>
<details>
<summary class="heading">
Logging
</summary>
<ul>
<li>Get a certain number of lines of service logs with the given scope.</li>
<li>Get a certain number of lines of service logs with the given scope.</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<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">
<label for="scope">Scope:</label>
<select name="scope">
<option value="default">
default
</option>
<option value="rascsi">
rascsi.service
</option>
<option value="rascsi-web">
rascsi-web.service
</option>
</select>
<input type="submit" value="Show Logs">
</form>
</td>
</tr>
</table>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post">
<label for="lines">Log Lines:</label>
<input type="number" placeholder="200" name="lines" min="1" size="4" />
<label for="scope">Scope:</label>
<select name="scope">
<option value="default">default</option>
<option value="rascsi">rascsi.service</option>
<option value="rascsi-web">rascsi-web.service</option>
</select>
<input type="submit" value="Show Logs" />
</form>
</td>
</tr>
</table>
<hr/>
<hr/>
<details>
<summary class="heading">Server Log Level</summary>
<details>
<summary class="heading">
Server Log Level
</summary>
<ul>
<li>Change the log level of the RaSCSI backend service.</li>
<li>The dropdown will indicate the current log level.</li>
<li>Change the log level of the RaSCSI backend service.</li>
<li>The dropdown will indicate the current log level.</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post">
<label for="level">Log Level:</label>
<select name="level">
{% for level in log_levels %}
<option value="{{ level }}"{% if level == current_log_level %} selected{% endif %}>
{{ level }}
</option>
{% endfor %}
</select>
<input type="submit" value="Set Log Level">
</form>
</td>
</tr>
</table>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/logs/level" method="post">
<label for="level">Log Level:</label>
<select name="level">
{% for level in log_levels %}
<option value="{{level}}"{% if level == current_log_level %} selected{% endif %}>{{level}}</option>
{% endfor %}
</select>
<input type="submit" value="Set Log Level" />
</form>
</td>
</tr>
</table>
<hr/>
<hr/>
<details>
<summary class="heading">Raspberry Pi Operations</summary>
<details>
<summary class="heading">
Raspberry Pi Operations
</summary>
<ul>
<li>Issue reboot or shutdown commands to the Raspberr Pi.</li>
<li>You can also restart the RaSCSI backend service here.</li>
<li>Issue reboot or shutdown commands to the Raspberr Pi.</li>
<li>You can also restart the RaSCSI backend service here.</li>
</ul>
</details>
</details>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/pi/reboot" method="post" onsubmit="return confirm('Reboot Pi?')">
<input type="submit" value="Reboot Raspberry Pi">
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/pi/shutdown" method="post" onsubmit="return confirm('Shutdown Pi?')">
<input type="submit" value="Shut Down Raspberry Pi">
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/rascsi/restart" method="post" onsubmit="return confirm('Restart RaSCSI?')">
<input type="submit" value="Restart RaSCSI Service">
</form>
</td>
</tr>
</table>
<table style="border: none">
<tr style="border: none">
<td style="border: none; vertical-align:top;">
<form action="/pi/reboot" method="post" onsubmit="return confirm('Reboot Pi?')">
<input type="submit" value="Reboot Raspberry Pi" />
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/pi/shutdown" method="post" onsubmit="return confirm('Shutdown Pi?')">
<input type="submit" value="Shut Down Raspberry Pi" />
</form>
</td>
<td style="border: none; vertical-align:top;">
<form action="/rascsi/restart" method="post" onsubmit="return confirm('Restart RaSCSI?')">
<input type="submit" value="Restart RaSCSI Service" />
</form>
</td>
</tr>
</table>
{% endblock %}
{% endblock content %}

View File

@ -1,4 +1,10 @@
"""
Module for the Flask app rendering and endpoints
"""
import logging
from pathlib import Path
from flask import (
Flask,
render_template,
@ -12,8 +18,8 @@ from flask import (
)
from file_cmds import (
list_config_files,
list_images,
list_config_files,
create_new_image,
download_file_to_iso,
delete_image,
@ -37,52 +43,67 @@ from pi_cmds import (
from ractl_cmds import (
attach_image,
list_devices,
sort_and_format_devices,
detach_by_id,
eject_by_id,
get_valid_scsi_ids,
detach_all,
get_server_info,
get_network_info,
get_device_types,
set_log_level,
)
from settings import *
from device_utils import (
sort_and_format_devices,
get_valid_scsi_ids,
)
from settings import (
CFG_DIR,
AFP_DIR,
MAX_FILE_SIZE,
ARCHIVE_FILE_SUFFIX,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
DEFAULT_CONFIG,
DRIVE_PROPERTIES_FILE,
REMOVABLE_DEVICE_TYPES,
)
app = Flask(__name__)
APP = Flask(__name__)
@app.route("/")
@APP.route("/")
def index():
"""
Sets up data structures for and renders the index page
"""
server_info = get_server_info()
disk = disk_space()
devices = list_devices()
device_types=get_device_types()
device_types = get_device_types()
files = list_images()
config_files = list_config_files()
sorted_image_files = sorted(files["files"], key = lambda x: x["name"].lower())
sorted_config_files = sorted(config_files, key = lambda x: x.lower())
sorted_image_files = sorted(files["files"], key=lambda x: x["name"].lower())
sorted_config_files = sorted(config_files, key=lambda x: x.lower())
from pathlib import Path
attached_images = []
luns = 0
for d in devices["device_list"]:
attached_images.append(Path(d["image"]).name)
luns += int(d["un"])
units = 0
# If there are more than 0 logical unit numbers, display in the Web UI
for device in devices["device_list"]:
attached_images.append(Path(device["image"]).name)
units += int(device["un"])
reserved_scsi_ids = server_info["reserved_ids"]
scsi_ids, recommended_id = get_valid_scsi_ids(devices["device_list"], reserved_scsi_ids)
formatted_devices = sort_and_format_devices(devices["device_list"])
valid_file_suffix = "."+", .".join(
server_info["sahd"] +
server_info["schd"] +
server_info["scrm"] +
server_info["scmo"] +
server_info["sccd"] +
list(ARCHIVE_FILE_SUFFIX)
)
server_info["sahd"] +
server_info["schd"] +
server_info["scrm"] +
server_info["scmo"] +
server_info["sccd"] +
[ARCHIVE_FILE_SUFFIX]
)
return render_template(
"index.html",
@ -92,14 +113,14 @@ def index():
files=sorted_image_files,
config_files=sorted_config_files,
base_dir=server_info["image_dir"],
cfg_dir=cfg_dir,
afp_dir=afp_dir,
CFG_DIR=CFG_DIR,
AFP_DIR=AFP_DIR,
scsi_ids=scsi_ids,
recommended_id=recommended_id,
attached_images=attached_images,
luns=luns,
units=units,
reserved_scsi_ids=reserved_scsi_ids,
max_file_size=int(MAX_FILE_SIZE / 1024 / 1024),
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
running_env=running_env(),
version=server_info["version"],
log_levels=server_info["log_levels"],
@ -109,11 +130,11 @@ def index():
free_disk=int(disk["free"] / 1024 / 1024),
valid_file_suffix=valid_file_suffix,
archive_file_suffix=ARCHIVE_FILE_SUFFIX,
removable_device_types=REMOVABLE_DEVICE_TYPES,
REMOVABLE_DEVICE_TYPES=REMOVABLE_DEVICE_TYPES,
)
@app.route("/drive/list", methods=["GET"])
@APP.route("/drive/list", methods=["GET"])
def drive_list():
"""
Sets up the data structures and kicks off the rendering of the drive list page
@ -123,11 +144,10 @@ def drive_list():
# Reads the canonical drive properties into a dict
# The file resides in the current dir of the web ui process
from pathlib import Path
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if drive_properties.is_file():
process = read_drive_properties(str(drive_properties))
if process["status"] == False:
if not process["status"]:
flash(process["msg"], "error")
return redirect(url_for("index"))
conf = process["conf"]
@ -140,24 +160,24 @@ def drive_list():
rm_conf = []
from werkzeug.utils import secure_filename
for d in conf:
if d["device_type"] == "SCHD":
d["secure_name"] = secure_filename(d["name"])
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
hd_conf.append(d)
elif d["device_type"] == "SCCD":
d["size_mb"] = "N/A"
cd_conf.append(d)
elif d["device_type"] == "SCRM":
d["secure_name"] = secure_filename(d["name"])
d["size_mb"] = "{:,.2f}".format(d["size"] / 1024 / 1024)
rm_conf.append(d)
for device in conf:
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
hd_conf.append(device)
elif device["device_type"] == "SCCD":
device["size_mb"] = "N/A"
cd_conf.append(device)
elif device["device_type"] == "SCRM":
device["secure_name"] = secure_filename(device["name"])
device["size_mb"] = "{:,.2f}".format(device["size"] / 1024 / 1024)
rm_conf.append(device)
files=list_images()
sorted_image_files = sorted(files["files"], key = lambda x: x["name"].lower())
hd_conf = sorted(hd_conf, key = lambda x: x["name"].lower())
cd_conf = sorted(cd_conf, key = lambda x: x["name"].lower())
rm_conf = sorted(rm_conf, key = lambda x: x["name"].lower())
files = list_images()
sorted_image_files = sorted(files["files"], key=lambda x: x["name"].lower())
hd_conf = sorted(hd_conf, key=lambda x: x["name"].lower())
cd_conf = sorted(cd_conf, key=lambda x: x["name"].lower())
rm_conf = sorted(rm_conf, key=lambda x: x["name"].lower())
return render_template(
"drives.html",
@ -173,13 +193,19 @@ def drive_list():
)
@app.route('/pwa/<path:path>')
@APP.route('/pwa/<path:path>')
def send_pwa_files(path):
"""
Sets up mobile web resources
"""
return send_from_directory('pwa', path)
@app.route("/drive/create", methods=["POST"])
@APP.route("/drive/create", methods=["POST"])
def drive_create():
"""
Creates the image and properties file pair
"""
vendor = request.form.get("vendor")
product = request.form.get("product")
revision = request.form.get("revision")
@ -190,29 +216,34 @@ def drive_create():
# Creating the image file
process = create_new_image(file_name, file_type, size)
if process["status"] == True:
flash(f"Drive image file {file_name}.{file_type} created")
flash(process["msg"])
if process["status"]:
flash(f"Created drive image file: {file_name}.{file_type}")
else:
flash(f"Failed to create file {file_name}.{file_type}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
# Creating the drive properties file
prop_file_name = f"{file_name}.{file_type}.{PROPERTIES_SUFFIX}"
properties = {"vendor": vendor, "product": product, \
"revision": revision, "block_size": block_size}
properties = {
"vendor": vendor,
"product": product,
"revision": revision,
"block_size": block_size,
}
process = write_drive_properties(prop_file_name, properties)
if process["status"] == True:
flash(f"Drive properties file {prop_file_name} created")
return redirect(url_for("index"))
else:
flash(f"Failed to create drive properties file {prop_file_name}", "error")
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(process['msg'], "error")
return redirect(url_for("index"))
@app.route("/drive/cdrom", methods=["POST"])
@APP.route("/drive/cdrom", methods=["POST"])
def drive_cdrom():
"""
Creates a properties file for a CD-ROM image
"""
vendor = request.form.get("vendor")
product = request.form.get("product")
revision = request.form.get("revision")
@ -222,63 +253,70 @@ def drive_cdrom():
# Creating the drive properties file
file_name = f"{file_name}.{PROPERTIES_SUFFIX}"
properties = {
"vendor": vendor,
"product": product,
"revision": revision,
"block_size": block_size,
}
"vendor": vendor,
"product": product,
"revision": revision,
"block_size": block_size,
}
process = write_drive_properties(file_name, properties)
if process["status"] == True:
flash(f"Drive properties file {file_name} created")
return redirect(url_for("index"))
else:
flash(f"Failed to create drive properties file {file_name}", "error")
return redirect(url_for("index"))
@app.route("/config/save", methods=["POST"])
def config_save():
file_name = request.form.get("name") or "default"
file_name = f"{file_name}.json"
process = write_config(file_name)
if process["status"] == True:
flash(f"Saved config to {file_name}!")
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to save config to {file_name}!", "error")
flash(process['msg'], "error")
flash(process['msg'], "error")
return redirect(url_for("index"))
@APP.route("/config/save", methods=["POST"])
def config_save():
"""
Saves a config file to disk
"""
file_name = request.form.get("name") or "default"
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
process = write_config(file_name)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(process['msg'], "error")
return redirect(url_for("index"))
@app.route("/config/load", methods=["POST"])
@APP.route("/config/load", methods=["POST"])
def config_load():
"""
Loads a config file from disk
"""
file_name = request.form.get("name")
if "load" in request.form:
process = read_config(file_name)
if process["status"] == True:
flash(f"Loaded config from {file_name}!")
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to load {file_name}!", "error")
flash(process['msg'], "error")
return redirect(url_for("index"))
flash(process['msg'], "error")
return redirect(url_for("index"))
elif "delete" in request.form:
process = delete_file(cfg_dir + file_name)
if process["status"] == True:
flash(f"Deleted config {file_name}!")
return redirect(url_for("index"))
else:
flash(f"Failed to delete {file_name}!", "error")
flash(process['msg'], "error")
process = delete_file(CFG_DIR + file_name)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(process['msg'], "error")
return redirect(url_for("index"))
@app.route("/logs/show", methods=["POST"])
flash("Got an unhandled request (needs to be either load or delete)", "error")
return redirect(url_for("index"))
@APP.route("/logs/show", methods=["POST"])
def show_logs():
"""
Displays system logs
"""
lines = request.form.get("lines") or "200"
scope = request.form.get("scope") or "default"
@ -291,60 +329,67 @@ def show_logs():
if process.returncode == 0:
headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers
else:
flash("Failed to get logs")
flash(process.stdout.decode("utf-8"), "stdout")
flash(process.stderr.decode("utf-8"), "stderr")
return redirect(url_for("index"))
flash("Failed to get logs")
flash(process.stdout.decode("utf-8"), "stdout")
flash(process.stderr.decode("utf-8"), "stderr")
return redirect(url_for("index"))
@app.route("/logs/level", methods=["POST"])
@APP.route("/logs/level", methods=["POST"])
def log_level():
"""
Sets RaSCSI backend log level
"""
level = request.form.get("level") or "info"
process = set_log_level(level)
if process["status"] == True:
flash(f"Log level set to {level}!")
return redirect(url_for("index"))
else:
flash(f"Failed to set log level to {level}!", "error")
flash(process["msg"], "error")
if process["status"]:
flash(f"Log level set to {level}")
return redirect(url_for("index"))
flash(f"Failed to set log level to {level}!", "error")
return redirect(url_for("index"))
@app.route("/daynaport/attach", methods=["POST"])
@APP.route("/daynaport/attach", methods=["POST"])
def daynaport_attach():
"""
Attaches a DaynaPORT ethernet adapter device
"""
scsi_id = request.form.get("scsi_id")
interface = request.form.get("if")
ip = request.form.get("ip")
ip_addr = request.form.get("ip")
mask = request.form.get("mask")
kwargs = {"device_type": "SCDP"}
if interface != "":
arg = interface
if "" not in (ip, mask):
arg += (":" + ip + "/" + mask)
if "" not in (ip_addr, mask):
arg += (":" + ip_addr + "/" + mask)
kwargs["interfaces"] = arg
process = attach_image(scsi_id, **kwargs)
if process["status"] == True:
if process["status"]:
flash(f"Attached DaynaPORT to SCSI ID {scsi_id}!")
return redirect(url_for("index"))
else:
flash(f"Failed to attach DaynaPORT to SCSI ID {scsi_id}!", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/scsi/attach", methods=["POST"])
@APP.route("/scsi/attach", methods=["POST"])
def attach():
"""
Attaches a file image as a device
"""
file_name = request.form.get("file_name")
file_size = request.form.get("file_size")
scsi_id = request.form.get("scsi_id")
un = request.form.get("un")
unit = request.form.get("unit")
device_type = request.form.get("type")
kwargs = {"unit": int(un), "image": file_name}
kwargs = {"unit": int(unit), "image": file_name}
# The most common block size is 512 bytes
expected_block_size = 512
@ -355,16 +400,13 @@ def attach():
expected_block_size = 2048
elif device_type == "SAHD":
expected_block_size = 256
# Attempt to load the device properties file:
# same base path but with PROPERTIES_SUFFIX appended
from pathlib import Path
drive_properties = f"{cfg_dir}{file_name}.{PROPERTIES_SUFFIX}"
# same file name with PROPERTIES_SUFFIX appended
drive_properties = f"{CFG_DIR}{file_name}.{PROPERTIES_SUFFIX}"
if Path(drive_properties).is_file():
process = read_drive_properties(drive_properties)
if process["status"] == False:
flash(f"Failed to load the device properties \
file {file_name_base}.{PROPERTIES_SUFFIX}", "error")
if not process["status"]:
flash(process["msg"], "error")
return redirect(url_for("index"))
conf = process["conf"]
@ -375,69 +417,80 @@ def attach():
expected_block_size = conf["block_size"]
process = attach_image(scsi_id, **kwargs)
if process["status"] == True:
flash(f"Attached {file_name} to SCSI ID {scsi_id} LUN {un}!")
if process["status"]:
flash(f"Attached {file_name} to SCSI ID {scsi_id} LUN {unit}")
if int(file_size) % int(expected_block_size):
flash(f"The image file size {file_size} bytes is not a multiple of \
{expected_block_size} and RaSCSI will ignore the trailing data. \
The image may be corrupted so proceed with caution.", "error")
return redirect(url_for("index"))
else:
flash(f"Failed to attach {file_name} to SCSI ID {scsi_id} LUN {un}!", "error")
flash(process["msg"], "error")
flash(f"The image file size {file_size} bytes is not a multiple of "
f"{expected_block_size} and RaSCSI will ignore the trailing data. "
f"The image may be corrupted so proceed with caution.", "error")
return redirect(url_for("index"))
flash(f"Failed to attach {file_name} to SCSI ID {scsi_id} LUN {unit}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/scsi/detach_all", methods=["POST"])
@APP.route("/scsi/detach_all", methods=["POST"])
def detach_all_devices():
"""
Detaches all currently attached devices
"""
process = detach_all()
if process["status"] == True:
flash("Detached all SCSI devices!")
return redirect(url_for("index"))
else:
flash("Failed to detach all SCSI devices!", "error")
flash(process["msg"], "error")
if process["status"]:
flash("Detached all SCSI devices")
return redirect(url_for("index"))
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/scsi/detach", methods=["POST"])
@APP.route("/scsi/detach", methods=["POST"])
def detach():
"""
Detaches a specified device
"""
scsi_id = request.form.get("scsi_id")
un = request.form.get("un")
process = detach_by_id(scsi_id, un)
if process["status"] == True:
flash(f"Detached SCSI ID {scsi_id} LUN {un}!")
return redirect(url_for("index"))
else:
flash(f"Failed to detach SCSI ID {scsi_id} LUN {un}!", "error")
flash(process["msg"], "error")
unit = request.form.get("unit")
process = detach_by_id(scsi_id, unit)
if process["status"]:
flash(f"Detached SCSI ID {scsi_id} LUN {unit}")
return redirect(url_for("index"))
flash(f"Failed to detach SCSI ID {scsi_id} LUN {unit}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/scsi/eject", methods=["POST"])
@APP.route("/scsi/eject", methods=["POST"])
def eject():
"""
Ejects a specified removable device image, but keeps the device attached
"""
scsi_id = request.form.get("scsi_id")
un = request.form.get("un")
unit = request.form.get("unit")
process = eject_by_id(scsi_id, un)
if process["status"] == True:
flash(f"Ejected SCSI ID {scsi_id} LUN {un}!")
return redirect(url_for("index"))
else:
flash(f"Failed to eject SCSI ID {scsi_id} LUN {un}!", "error")
flash(process["msg"], "error")
process = eject_by_id(scsi_id, unit)
if process["status"]:
flash(f"Ejected SCSI ID {scsi_id} LUN {unit}")
return redirect(url_for("index"))
@app.route("/scsi/info", methods=["POST"])
flash(f"Failed to eject SCSI ID {scsi_id} LUN {unit}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@APP.route("/scsi/info", methods=["POST"])
def device_info():
"""
Displays detailed info for a specific device
"""
scsi_id = request.form.get("scsi_id")
un = request.form.get("un")
unit = request.form.get("unit")
devices = list_devices(scsi_id, un)
devices = list_devices(scsi_id, unit)
# First check if any device at all was returned
if devices["status"] == False:
flash(f"No device attached to SCSI ID {scsi_id} LUN {un}!", "error")
if not devices["status"]:
flash(devices["msg"], "error")
return redirect(url_for("index"))
# Looking at the first dict in list to get
# the one and only device that should have been returned
@ -456,12 +509,15 @@ def device_info():
flash(f"Block Size: {device['block_size']} bytes")
flash(f"Image Size: {device['size']} bytes")
return redirect(url_for("index"))
else:
flash(f"Failed to get device info for SCSI ID {scsi_id} LUN {un}!", "error")
return redirect(url_for("index"))
@app.route("/pi/reboot", methods=["POST"])
flash(devices["msg"], "error")
return redirect(url_for("index"))
@APP.route("/pi/reboot", methods=["POST"])
def restart():
"""
Restarts the Pi
"""
detach_all()
flash("Safely detached all devices.")
flash("Rebooting the Pi momentarily...")
@ -469,8 +525,11 @@ def restart():
return redirect(url_for("index"))
@app.route("/rascsi/restart", methods=["POST"])
@APP.route("/rascsi/restart", methods=["POST"])
def rascsi_restart():
"""
Restarts the RaSCSI backend service
"""
detach_all()
flash("Safely detached all devices.")
flash("Restarting RaSCSI Service...")
@ -479,8 +538,11 @@ def rascsi_restart():
return redirect(url_for("index"))
@app.route("/pi/shutdown", methods=["POST"])
@APP.route("/pi/shutdown", methods=["POST"])
def shutdown():
"""
Shuts down the Pi
"""
detach_all()
flash("Safely detached all devices.")
flash("Shutting down the Pi momentarily...")
@ -488,62 +550,73 @@ def shutdown():
return redirect(url_for("index"))
@app.route("/files/download_to_iso", methods=["POST"])
@APP.route("/files/download_to_iso", methods=["POST"])
def download_to_iso():
"""
Downloads a remote file and creates a CD-ROM image formatted with HFS that contains the file
"""
scsi_id = request.form.get("scsi_id")
url = request.form.get("url")
process = download_file_to_iso(scsi_id, url)
if process["status"] == True:
flash(f"Created CD-ROM image {process['file_name']}")
flash(process["msg"])
process = download_file_to_iso(url)
if process["status"]:
flash(f"Created CD-ROM image: {process['file_name']}")
else:
flash(f"Failed to create CD-ROM image from {url}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
process_attach = attach_image(scsi_id, device_type="SCCD", image=process["file_name"])
if process_attach["status"] == True:
if process_attach["status"]:
flash(f"Attached to SCSI ID {scsi_id}")
return redirect(url_for("index"))
else:
flash(f"Failed to attach to SCSI ID {scsi_id}. Try attaching it manually.", "error")
flash(process_attach["msg"], "error")
return redirect(url_for("index"))
flash(f"Failed to attach image to SCSI ID {scsi_id}. Try attaching it manually.", "error")
flash(process_attach["msg"], "error")
return redirect(url_for("index"))
@app.route("/files/download_to_images", methods=["POST"])
@APP.route("/files/download_to_images", methods=["POST"])
def download_img():
"""
Downloads a remote file onto the images dir on the Pi
"""
url = request.form.get("url")
server_info = get_server_info()
process = download_to_dir(url, server_info["image_dir"])
if process["status"] == True:
flash(f"File Downloaded from {url} to {server_info['image_dir']}")
return redirect(url_for("index"))
else:
flash(f"Failed to download file {url}", "error")
flash(process["msg"], "error")
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(f"Failed to download file {url}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/files/download_to_afp", methods=["POST"])
@APP.route("/files/download_to_afp", methods=["POST"])
def download_afp():
"""
Downloads a remote file onto the AFP shared dir on the Pi
"""
url = request.form.get("url")
process = download_to_dir(url, afp_dir)
if process["status"] == True:
flash(f"File Downloaded from {url} to {afp_dir}")
return redirect(url_for("index"))
else:
flash(f"Failed to download file {url}", "error")
flash(process["msg"], "error")
process = download_to_dir(url, AFP_DIR)
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(f"Failed to download file {url}", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/files/upload", methods=["POST"])
@APP.route("/files/upload", methods=["POST"])
def upload_file():
"""
Uploads a file from the local computer to the images dir on the Pi
Depending on the Dropzone.js JavaScript library
"""
from werkzeug.utils import secure_filename
from os import path
import pydrop
log = logging.getLogger("pydrop")
file = request.files["file"]
@ -554,15 +627,15 @@ def upload_file():
save_path = path.join(server_info["image_dir"], filename)
current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
# 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((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())
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file.stream.read())
except OSError:
log.exception("Could not write to file")
return make_response(("Unable to write the file to disk!", 500))
@ -572,22 +645,24 @@ def upload_file():
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']}.")
log.error("Finished transferring %s, "
"but it has a size mismatch with the original file."
"Got %s but we expected %s.",
file.filename, path.getsize(save_path), request.form['dztotalfilesize'])
return make_response(("Transferred file corrupted!", 500))
else:
log.info(f"File {file.filename} has been uploaded successfully")
else:
log.debug(f"Chunk {current_chunk + 1} of {total_chunks} "
f"for file {file.filename} completed.")
log.info("File %s has been uploaded successfully", file.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file.filename)
return make_response(("File upload successful!", 200))
@app.route("/files/create", methods=["POST"])
@APP.route("/files/create", methods=["POST"])
def create_file():
"""
Creates an empty image file in the images dir
"""
file_name = request.form.get("file_name")
size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type")
@ -596,90 +671,84 @@ def create_file():
file_name = secure_filename(file_name)
process = create_new_image(file_name, file_type, size)
if process["status"] == True:
flash(f"Drive image created as {file_name}.{file_type}")
flash(process["msg"])
return redirect(url_for("index"))
else:
flash(f"Failed to create file {file_name}.{file_type}", "error")
flash(process["msg"], "error")
if process["status"]:
flash(f"Drive image created: {file_name}.{file_type}")
return redirect(url_for("index"))
flash(process["msg"], "error")
return redirect(url_for("index"))
@app.route("/files/download", methods=["POST"])
@APP.route("/files/download", methods=["POST"])
def download():
"""
Downloads a file from the Pi to the local computer
"""
image = request.form.get("image")
server_info = get_server_info()
return send_file(f"{server_info['image_dir']}/{image}", as_attachment=True)
@app.route("/files/delete", methods=["POST"])
@APP.route("/files/delete", methods=["POST"])
def delete():
"""
Deletes a specified file in the images dir
"""
file_name = request.form.get("image")
process = delete_image(file_name)
if process["status"] == True:
flash(f"File {file_name} deleted!")
flash(process["msg"])
if process["status"]:
flash(f"Image file deleted: {file_name}")
else:
flash(f"Failed to delete file {file_name}!", "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
# Delete the drive properties file, if it exists
from pathlib import Path
prop_file_path = f"{cfg_dir}{file_name}.{PROPERTIES_SUFFIX}"
prop_file_path = f"{CFG_DIR}{file_name}.{PROPERTIES_SUFFIX}"
if Path(prop_file_path).is_file():
process = delete_file(prop_file_path)
if process["status"] == True:
flash(f"File {prop_file_path} deleted!")
return redirect(url_for("index"))
else:
flash(f"Failed to delete file {prop_file_path}!", "error")
flash(process["msg"], "error")
if process["status"]:
flash(process["msg"])
return redirect(url_for("index"))
flash(process["msg"], "error")
return redirect(url_for("index"))
return redirect(url_for("index"))
@app.route("/files/unzip", methods=["POST"])
@APP.route("/files/unzip", methods=["POST"])
def unzip():
"""
Unzips a specified zip file
"""
image = request.form.get("image")
member = request.form.get("member") or False
process = unzip_file(image, member)
if process["status"]:
if len(process["msg"]) < 1:
if not process["msg"]:
flash("Aborted unzip: File(s) with the same name already exists.", "error")
return redirect(url_for("index"))
flash("Unzipped the following files:")
for m in process["msg"]:
flash(m)
return redirect(url_for("index"))
else:
flash("Failed to unzip " + image, "error")
flash(process["msg"], "error")
for msg in process["msg"]:
flash(msg)
return redirect(url_for("index"))
flash("Failed to unzip " + image, "error")
flash(process["msg"], "error")
return redirect(url_for("index"))
if __name__ == "__main__":
app.secret_key = "rascsi_is_awesome_insecure_secret_key"
app.config["SESSION_TYPE"] = "filesystem"
server_info = get_server_info()
app.config["UPLOAD_FOLDER"] = server_info["image_dir"]
from os import makedirs
makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
app.config["MAX_CONTENT_LENGTH"] = MAX_FILE_SIZE
APP.secret_key = "rascsi_is_awesome_insecure_secret_key"
APP.config["SESSION_TYPE"] = "filesystem"
APP.config["MAX_CONTENT_LENGTH"] = int(MAX_FILE_SIZE)
# Load the default configuration file, if found
from pathlib import Path
default_config_path = Path(cfg_dir + DEFAULT_CONFIG)
if default_config_path.is_file():
if Path(DEFAULT_CONFIG).is_file():
read_config(DEFAULT_CONFIG)
import bjoern
print("Serving rascsi-web...")
bjoern.run(app, "0.0.0.0", 8080)
bjoern.run(APP, "0.0.0.0", 8080)