diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f9857ddc..20e3089e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @akuker @erichelgeson +* @akuker @erichelgeson @rdmark diff --git a/README.md b/README.md index e7f675ef..5a428d39 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,18 @@ RaSCSI Reloaded is using the . (for the first release of the month). Hot fixes, if necessary, will be released as ... For example, the first release in January 2021 will be release "21.01". If a hot-fix is needed for this release, the first hotfix will be "21.01.1". +- A tag will be created for each "release". The releases will be named .. where the release number is incremented for each subsequent release tagged in the same calendar month. The first release of the month of January 2021 is called "21.01.01", the second one in the same month "21.01.02" and so on. Typically, releases will only be planned every few months. +When you are ready to contribute code to RaSCSI Reloaded, follow the GitHub Forking and Pull Request workflow to create your own fork where you can make changes, and then contribute it back to the project. Please remember to always make Pull Requests against the *develop* branch. + +If you want to add a new translation, or improve upon an existing one, please follow the instructions in the Web Interface README. Once the translation is complete, please use the same workflow as above to contribute it to the project. + I sell on Tindie -# Github Sponsors -Thank you to all of the Github sponsors who support the development community! +# GitHub Sponsors +Thank you to all of the GitHub sponsors who support the development community! Special thank you to the Gold level sponsors! - @mikelord68 diff --git a/easyinstall.sh b/easyinstall.sh index 71464b6b..458bc747 100755 --- a/easyinstall.sh +++ b/easyinstall.sh @@ -13,7 +13,7 @@ logo="""  ~ (║|_____|║) ~\n ( : ║ .  __ ║ : )\n  ~ .╚╦═════╦╝. ~\n -  (  ¯¯¯¯¯¯¯  ) RaSCSI Assistant\n +  (  ¯¯¯¯¯¯¯  ) RaSCSI Reloaded Assistant\n    '~ .~~~. ~'\n        '~'\n """ @@ -55,6 +55,8 @@ OLED_INSTALL_PATH="$BASE/python/oled" CTRLBOARD_INSTALL_PATH="$BASE/python/ctrlboard" PYTHON_COMMON_PATH="$BASE/python/common" SYSTEMD_PATH="/etc/systemd/system" +SSL_CERTS_PATH="/etc/ssl/certs" +SSL_KEYS_PATH="/etc/ssl/private" HFS_FORMAT=/usr/bin/hformat HFDISK_BIN=/usr/bin/hfdisk LIDO_DRIVER=$BASE/lido-driver.img @@ -80,7 +82,7 @@ function sudoCheck() { # install all dependency packages for RaSCSI Service function installPackages() { - sudo apt-get update && sudo apt-get install git libspdlog-dev libpcap-dev genisoimage python3 python3-venv python3-dev python3-pip nginx libpcap-dev protobuf-compiler bridge-utils libev-dev libevdev2 -y .+)".$' + unar_result_no_files = "No files extracted." + unar_file_extracted = \ + r"^ (?P.+). \(((?P[0-9]+) B)?(?P(dir)?(, )?(rsrc)?)\)\.\.\. (?P[A-Z]+)\.$" + + lines = process["stdout"].rstrip("\n").split("\n") + + if lines[-1] == unar_result_no_files: + raise UnarNoFilesExtractedError + + if match(unar_result_success, lines[-1]): + extracted_members = [] + + for line in lines[1:-1]: + line_matches = match(unar_file_extracted, line) + if line_matches: + matches = line_matches.groupdict() + member = { + "name": str(pathlib.PurePath(matches["path"]).name), + "path": matches["path"], + "size": matches["size"] or 0, + "is_dir": False, + "is_resource_fork": False, + "absolute_path": str(pathlib.PurePath(tmp_dir).joinpath(matches["path"])), + } + + member_types = matches.get("types", "") + if member_types.startswith(", "): + member_types = member_types[2:].split(", ") + else: + member_types = member_types.split(", ") + + if "dir" in member_types: + member["is_dir"] = True + + if "rsrc" in member_types: + if not fork_output_type: + continue + + member["is_resource_fork"] = True + + # Update names/paths to match unar resource fork naming convention + if fork_output_type == FORK_OUTPUT_TYPE_HIDDEN: + member["name"] = f"._{member['name']}" + else: + member["name"] += ".rsrc" + member["path"] = str(pathlib.PurePath(member["path"]).parent.joinpath(member["name"])) + member["absolute_path"] = str(pathlib.PurePath(tmp_dir).joinpath(member["path"])) + + logging.debug("Extracted: %s -> %s", member['path'], member['absolute_path']) + extracted_members.append(member) + else: + raise UnarUnexpectedOutputError(f"Unexpected output: {line}") + + moved = [] + skipped = [] + for member in sorted(extracted_members, key=lambda m: m["path"]): + source_path = pathlib.Path(member["absolute_path"]) + target_path = pathlib.Path(output_dir).joinpath(member["path"]) + member["absolute_path"] = str(target_path) + + if target_path.exists(): + logging.info("Skipping temp file/dir as the target already exists: %s", target_path) + skipped.append(member) + continue + + if member["is_dir"]: + logging.debug("Creating empty dir: %s -> %s", source_path, target_path) + target_path.mkdir(parents=True, exist_ok=True) + moved.append(member) + continue + + # The parent dir may not be specified as a member, so ensure it exists + target_path.parent.mkdir(parents=True, exist_ok=True) + logging.debug("Moving temp file: %s -> %s", source_path, target_path) + source_path.rename(target_path) + moved.append(member) + + return { + "extracted": moved, + "skipped": skipped, + } + + raise UnarUnexpectedOutputError(lines[-1]) + + +def inspect_archive(file_path, **kwargs): + """ + Calls `lsar` to inspect the contents of an archive + Takes (str) file_path + Returns (dict) of (str) format, (list) members + """ + if not pathlib.Path(file_path): + raise FileNotFoundError(f"File {file_path} does not exist") + + process = run("lsar", ["-json", "--", file_path]) + + if process["returncode"] != 0: + raise LsarCommandError(f"Non-zero return code: {process['returncode']}") + + try: + archive_info = loads(process["stdout"]) + except JSONDecodeError as error: + raise LsarOutputError(f"Unable to read JSON output from lsar: {error.msg}") from error + + members = [{ + "name": pathlib.PurePath(member.get("XADFileName")).name, + "path": member.get("XADFileName"), + "size": member.get("XADFileSize"), + "is_dir": member.get("XADIsDirectory"), + "is_resource_fork": member.get("XADIsResourceFork"), + "raw": member, + } for member in archive_info.get("lsarContents", [])] + + return { + "format": archive_info.get("lsarFormatName"), + "members": members, + } + + +class UnarCommandError(Exception): + """ Command execution was unsuccessful """ + pass + + +class UnarNoFilesExtractedError(Exception): + """ Command completed, but no files extracted """ + + +class UnarUnexpectedOutputError(Exception): + """ Command output not recognized """ + + +class LsarCommandError(Exception): + """ Command execution was unsuccessful """ + + +class LsarOutputError(Exception): + """ Command output could not be parsed""" diff --git a/python/web/README.md b/python/web/README.md index 5be61e1e..6487bf9c 100644 --- a/python/web/README.md +++ b/python/web/README.md @@ -21,7 +21,9 @@ 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 solution will be needed for this interface. -## Pushing to the Pi via git +## (Optional) Pushing to the Pi via git + +This is a setup for pushing code changes from your local development environment to the Raspberry Pi without a roundtrip to the remote GitHub repository. Setup a bare repo on the rascsi ``` @@ -40,7 +42,7 @@ $ git push pi master ## Localizing the Web Interface -We use the Flask-Babel library and Flask/Jinja2 extension for i18n. +We use the Flask-Babel library and Flask/Jinja2 extension for internationalization (i18n). It uses the 'pybabel' command line tool for extracting and compiling localizations. The Web Interface start script will automatically compile localizations upon launch. @@ -55,7 +57,7 @@ To create a new localization, it needs to be added to the LANGAUGES constant in web/settings.py. To localize messages coming from the RaSCSI backend, update also code in raspberrypi/localizer.cpp in the RaSCSI C++ code. -Once this is done, it is time to localize the Python code. The below steps are derived from the [Flask-Babel documentation](https://flask-babel.tkte.ch/#translating-applications). +Once this is done, it is time to localize the Python code. The below steps are derived from the [Flask-Babel documentation](https://python-babel.github.io/flask-babel/index.html#translating-applications). First, generate the raw messages.pot file containing extracted strings. @@ -68,7 +70,7 @@ $ pybabel extract -F babel.cfg -o messages.pot . When adding a localization for a new language, initialize the directory structure. Replace 'xx' with the two character code for the language. ``` -$ pybabel init -i messages.pot -d translations -l xx +$ pybabel init -i messages.pot -d src/translations -l xx ``` ### Update an existing loclization @@ -114,6 +116,10 @@ msgstr "" "drivrutiner och inställningar." ``` +### Contributing to the project + +New or updated localizations are treated just like any other code change. See the [project README](https://github.com/akuker/RASCSI/tree/rdmark-readme-contributions#how-do-i-contribute) for further information. + ### (Optional) See translation stats for a localization Install the gettext package and use msgfmt to see the translation progress. ``` diff --git a/python/web/service-infra/nginx-default.conf b/python/web/service-infra/nginx-default.conf index 58804911..2e3c62f1 100644 --- a/python/web/service-infra/nginx-default.conf +++ b/python/web/service-infra/nginx-default.conf @@ -3,6 +3,16 @@ server { listen [::]:80 default_server; listen 80 default_server; + listen 443 ssl http2; + listen [::]:443 ssl http2; + + ssl_certificate /etc/ssl/certs/rascsi-web.crt; + ssl_certificate_key /etc/ssl/private/rascsi-web.key; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; location / { proxy_pass http://127.0.0.1:8080; diff --git a/python/web/src/drive_properties.json b/python/web/src/drive_properties.json index eb3880fd..0b5a1c60 100644 --- a/python/web/src/drive_properties.json +++ b/python/web/src/drive_properties.json @@ -430,5 +430,17 @@ "file_type": null, "description": "Emulates Apple CD ROM drive for use with Macintosh computers.", "url": "" +}, +{ + "device_type": "SCCD", + "vendor": null, + "product": null, + "revision": null, + "block_size": 512, + "size": null, + "name": "Generic CD-ROM 512 block size", + "file_type": null, + "description": "For use with host systems that expect the non-standard 512 byte block size for CD-ROM drives, such as Akai samplers.", + "url": "" } ] diff --git a/python/web/src/return_code_mapper.py b/python/web/src/return_code_mapper.py index 6b596f1c..94d9a5f3 100644 --- a/python/web/src/return_code_mapper.py +++ b/python/web/src/return_code_mapper.py @@ -9,25 +9,43 @@ class ReturnCodeMapper: """Class for mapping between rascsi return codes and translated strings""" MESSAGES = { - ReturnCodes.DELETEFILE_SUCCESS: _("File deleted: %(file_path)s"), - ReturnCodes.DELETEFILE_FILE_NOT_FOUND: _("File to delete not found: %(file_path)s"), - ReturnCodes.RENAMEFILE_SUCCESS: _("File moved to: %(target_path)s"), - ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE: _("Unable to move file to: %(target_path)s"), - ReturnCodes.DOWNLOADFILETOISO_SUCCESS: _("Created CD-ROM ISO image with " - "arguments \"%(value)s\""), - ReturnCodes.DOWNLOADTODIR_SUCCESS: _("%(file_name)s downloaded to %(save_dir)s"), - ReturnCodes.WRITEFILE_SUCCESS: _("File created: %(target_path)s"), - ReturnCodes.WRITEFILE_COULD_NOT_WRITE: _("Could not create file: %(target_path)s"), - ReturnCodes.READCONFIG_SUCCESS: _("Loaded configurations from: %(file_name)s"), - ReturnCodes.READCONFIG_COULD_NOT_READ: _("Could not read configuration " - "file: %(file_name)s"), - ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT: _("Invalid configuration file format"), - ReturnCodes.READDRIVEPROPS_SUCCESS: _("Read properties from file: %(file_path)s"), - ReturnCodes.READDRIVEPROPS_COULD_NOT_READ: _("Could not read properties from " - "file: %(file_path)s"), - ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH: _("Cannot insert an image for %(device_type)s " - "into a %(current_device_type)s device"), - } + ReturnCodes.DELETEFILE_SUCCESS: + _("File deleted: %(file_path)s"), + ReturnCodes.DELETEFILE_FILE_NOT_FOUND: + _("File to delete not found: %(file_path)s"), + ReturnCodes.RENAMEFILE_SUCCESS: + _("File moved to: %(target_path)s"), + ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE: + _("Unable to move file to: %(target_path)s"), + ReturnCodes.DOWNLOADFILETOISO_SUCCESS: + _("Created CD-ROM ISO image with arguments \"%(value)s\""), + ReturnCodes.DOWNLOADTODIR_SUCCESS: + _("%(file_name)s downloaded to %(save_dir)s"), + ReturnCodes.WRITEFILE_SUCCESS: + _("File created: %(target_path)s"), + ReturnCodes.WRITEFILE_COULD_NOT_WRITE: + _("Could not create file: %(target_path)s"), + ReturnCodes.READCONFIG_SUCCESS: + _("Loaded configurations from: %(file_name)s"), + ReturnCodes.READCONFIG_COULD_NOT_READ: + _("Could not read configuration file: %(file_name)s"), + ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT: + _("Invalid configuration file format"), + ReturnCodes.READDRIVEPROPS_SUCCESS: + _("Read properties from file: %(file_path)s"), + ReturnCodes.READDRIVEPROPS_COULD_NOT_READ: + _("Could not read properties from file: %(file_path)s"), + ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH: + _("Cannot insert an image for %(device_type)s into a %(current_device_type)s device"), + ReturnCodes.EXTRACTIMAGE_SUCCESS: + _("Extracted %(count)s file(s)"), + ReturnCodes.EXTRACTIMAGE_NO_FILES_SPECIFIED: + _("Unable to extract archive: No files were specified"), + ReturnCodes.EXTRACTIMAGE_NO_FILES_EXTRACTED: + _("No files were extracted (existing files are skipped)"), + ReturnCodes.EXTRACTIMAGE_COMMAND_ERROR: + _("Unable to extract archive: %(error)s"), + } @staticmethod def add_msg(payload): @@ -36,10 +54,14 @@ class ReturnCodeMapper: if "return_code" not in payload: return payload - parameters = payload["parameters"] + parameters = payload.get("parameters") - payload["msg"] = lazy_gettext( + if parameters: + payload["msg"] = lazy_gettext( ReturnCodeMapper.MESSAGES[payload["return_code"]], **parameters, ) + else: + payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]]) + return payload diff --git a/python/web/src/settings.py b/python/web/src/settings.py index f7988200..5ce3fea4 100644 --- a/python/web/src/settings.py +++ b/python/web/src/settings.py @@ -12,8 +12,6 @@ AFP_DIR = f"{HOME_DIR}/afpshare" MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb -ARCHIVE_FILE_SUFFIX = "zip" - # The file name of the default config file that loads when rascsi-web starts DEFAULT_CONFIG = f"default.{rascsi.common_settings.CONFIG_FILE_SUFFIX}" # File containing canonical drive properties diff --git a/python/web/src/static/style.css b/python/web/src/static/style.css index cf7ebf5c..48fc91c6 100644 --- a/python/web/src/static/style.css +++ b/python/web/src/static/style.css @@ -35,12 +35,14 @@ table, tr, td { color: white; font-size:20px; background-color:red; + white-space: pre-line; } .message { color: white; font-size:20px; background-color:green; + white-space: pre-line; } td.inactive { diff --git a/python/web/src/templates/drives.html b/python/web/src/templates/drives.html index accaee99..0d007ce4 100644 --- a/python/web/src/templates/drives.html +++ b/python/web/src/templates/drives.html @@ -34,7 +34,7 @@ - + {{ _("Size:") }} {{ _("B") }} .{{ hd.file_type }} @@ -124,7 +124,7 @@ - + {{ _("Size:") }} {{ _("B") }} .{{ rm.file_type }} diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index 49c2d096..e2d86f57 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -185,41 +185,41 @@ - {% elif file["zip_members"] %} + {% elif file["archive_contents"] %}
{{ file["name"] }}
    - {% for member in file["zip_members"] %} - {% if not member.lower().endswith(PROPERTIES_SUFFIX) %} -
  • - {% if member + "." + PROPERTIES_SUFFIX in file["zip_members"] %} -
    {{ member }} -
    - - - -
    -
    -
      + {% for member in file["archive_contents"] %} + {% if not member["is_properties_file"] %}
    • - {{ member + "." + PROPERTIES_SUFFIX }} + {% if member["related_properties_file"] %} +
      + + +
      + + + +
      +
      +
        +
      • {{ member["related_properties_file"] }}
      • +
      +
      + {% else %} + +
      + + + +
      + {% endif %}
    • -
    -
    - {% else %} - -
    - - - -
    {% endif %} -
  • - {% endif %} - {% endfor %} + {% endfor %}
@@ -238,11 +238,12 @@ {{ _("Attached!") }} {% else %} - {% if file["name"].lower().endswith(ARCHIVE_FILE_SUFFIX) %} -
- - - + {% if file["archive_contents"] %} + + + {% set pipe = joiner("|") %} + +
{% else %}
@@ -257,7 +258,7 @@ {% endfor %} - + {% if file["detected_type"] != "UNDEFINED" %} {{ file['detected_type_name'] }} @@ -336,7 +337,7 @@ {% for key, value in device_types[type]["params"].items() %} {% if value.isnumeric() %} - + {% elif key == "interface" %} - +
@@ -415,7 +416,7 @@ gb: "{{ _("GB") }}", mb: "{{ _("MB") }}", kb: "{{ _("KB") }}", - b: "{{ _("b") }}" + b: "{{ _("B") }}" } } @@ -571,7 +572,7 @@ - + @@ -606,7 +607,7 @@
- +