Partition and format HFS/FAT volumes in the Web UI + SMB install (#946)

- New "format as" option when creating new images; removing the image creation options from easyinstall
- Bring in HFSer as new submodule providing the driver binaries; removing the Lido driver binary from this repo
- Add SpeedTools driver option
- Point to github mirror of hfdisk, since the original git server is down
- While rearranging the easyinstall options, moved the CtrlBoard option up to the main section
- Add an easyinstall script to configure Samba, while consolidating file sharing with Netatalk
This commit is contained in:
Daniel Markstedt 2022-11-01 16:43:24 -07:00 committed by GitHub
parent 9a4f433baf
commit 85edd50047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 534 additions and 159 deletions

5
.gitignore vendored
View File

@ -7,7 +7,6 @@ core
__pycache__
current
rascsi_interface_pb2.py
src/raspberrypi/hfdisk/
*~
messages.pot
messages.mo
@ -27,3 +26,7 @@ s.sh
# temporary kicad files
*-backups
# submodules
hfdisk*
mac-hard-disk-drivers

View File

@ -28,7 +28,7 @@ RUN ./easyinstall.sh --run_choice=11
RUN ./easyinstall.sh --run_choice=13
# Setup wired network bridge
RUN ./easyinstall.sh --run_choice=6 --headless
RUN ./easyinstall.sh --run_choice=5 --headless
USER root
WORKDIR /home/pi

View File

@ -58,13 +58,13 @@ 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
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
GIT_REMOTE=${GIT_REMOTE:-origin}
TOKEN=""
SECRET_FILE="$HOME/.config/rascsi/rascsi_secret"
FILE_SHARE_PATH="$HOME/shared_files"
FILE_SHARE_NAME="Pi File Server"
set -e
@ -106,7 +106,10 @@ function installPackages() {
unar \
disktype \
libgmock-dev \
man2html
man2html \
hfsutils \
dosfstools \
kpartx
}
# install Debian packges for RaSCSI standalone
@ -592,98 +595,30 @@ function createDriveCustom() {
createDrive "$driveSize" "$driveName"
}
# Creates an HFS file system
function formatDrive() {
diskPath="$1"
volumeName="$2"
if [ ! -x $HFS_FORMAT ]; then
# Install hfsutils to have hformat to format HFS
sudo apt-get install hfsutils --assume-yes </dev/null
fi
if [ ! -x $HFDISK_BIN ]; then
# Clone, compile and install 'hfdisk', partition tool
git clone git://www.codesrc.com/git/hfdisk.git
cd hfdisk || exit 1
# Clone, compile and install 'hfdisk', partition tool
function installHfdisk() {
HFDISK_VERSION="2022.11"
if [ ! -x "$HFDISK_BIN" ]; then
cd "$BASE" || exit 1
wget -O "hfdisk-$HFDISK_VERSION.tar.gz" "https://github.com/rdmark/hfdisk/archive/refs/tags/$HFDISK_VERSION.tar.gz" </dev/null
tar -xzvf "hfdisk-$HFDISK_VERSION.tar.gz"
rm "hfdisk-$HFDISK_VERSION.tar.gz"
cd "hfdisk-$HFDISK_VERSION" || exit 1
make
sudo cp hfdisk /usr/bin/hfdisk
fi
sudo cp hfdisk "$HFDISK_BIN"
# Inject hfdisk commands to create Drive with correct partitions
# https://www.codesrc.com/mediawiki/index.php/HFSFromScratch
# i initialize partition map
# continue with default first block
# C Create 1st partition with type specified next)
# continue with default
# 32 32 blocks (required for HFS+)
# Driver_Partition Partition Name
# Apple_Driver Partition Type (available types: Apple_Driver, Apple_Driver43, Apple_Free, Apple_HFS...)
# C Create 2nd partition with type specified next
# continue with default first block
# continue with default block size (rest of the disk)
# ${volumeName} Partition name provided by user
# Apple_HFS Partition Type
# w Write partition map to disk
# y Confirm partition table
# p Print partition map
(echo i; echo ; echo C; echo ; echo 32; echo "Driver_Partition"; echo "Apple_Driver"; echo C; echo ; echo ; echo "${volumeName}"; echo "Apple_HFS"; echo w; echo y; echo p;) | $HFDISK_BIN "$diskPath"
partitionOk=$?
if [ $partitionOk -eq 0 ]; then
if [ ! -f "$LIDO_DRIVER" ];then
echo "Lido driver couldn't be found. Make sure RaSCSI is up-to-date with git pull"
return 1
fi
# Burn Lido driver to the disk
dd if="$LIDO_DRIVER" of="$diskPath" seek=64 count=32 bs=512 conv=notrunc
driverInstalled=$?
if [ $driverInstalled -eq 0 ]; then
# Format the partition with HFS file system
$HFS_FORMAT -l "${volumeName}" "$diskPath" 1
hfsFormattedOk=$?
if [ $hfsFormattedOk -eq 0 ]; then
echo "Disk created with success."
else
echo "Unable to format HFS partition."
return 4
fi
else
echo "Unable to install Lido Driver."
return 3
fi
else
echo "Unable to create the partition."
return 2
echo "Installed $HFDISK_BIN"
fi
}
# Creates an image file
function createDrive() {
if [ $# -ne 2 ]; then
echo "To create a Drive, volume size and volume name must be provided"
echo "$ createDrive 600 \"RaSCSI Drive\""
echo "Drive wasn't created."
return
fi
driveSize=$1
driveName=$2
mkdir -p "$VIRTUAL_DRIVER_PATH"
drivePath="${VIRTUAL_DRIVER_PATH}/${driveSize}M.hda"
if [ ! -f "$drivePath" ]; then
echo "Creating a ${driveSize}MiB Drive"
truncate --size "${driveSize}m" "$drivePath"
echo "Formatting drive with HFS"
formatDrive "$drivePath" "$driveName"
else
echo "Error: drive already exists"
# Fetch HFS drivers that the Web Interface uses
function fetchHardDiskDrivers() {
if [ ! -f "$BASE/mac-hard-disk-drivers" ]; then
cd "$BASE" || exit 1
wget https://macintoshgarden.org/sites/macintoshgarden.org/files/apps/mac-hard-disk-drivers.zip
unzip -d mac-hard-disk-drivers mac-hard-disk-drivers.zip
rm mac-hard-disk-drivers.zip
fi
}
@ -836,14 +771,12 @@ function setupWirelessNetworking() {
# Downloads, compiles, and installs Netatalk (AppleShare server)
function installNetatalk() {
NETATALK_VERSION="2-220801"
AFP_SHARE_PATH="$HOME/afpshare"
AFP_SHARE_NAME="Pi File Server"
NETATALK_CONFIG_PATH="/etc/netatalk"
if [ -d "$NETATALK_CONFIG_PATH" ]; then
echo
echo "WARNING: Netatalk configuration dir $NETATALK_CONFIG_PATH already exists."
echo "This installation process will overwrite existing Netatalk applications and configurations."
echo "This installation process will overwrite existing binaries and configurations."
echo "No shared files will be deleted, but you may have to manually restore your settings after the installation."
echo
echo "Do you want to proceed with the installation? [y/N]"
@ -853,13 +786,27 @@ function installNetatalk() {
fi
fi
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
echo
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
else
exit 0
fi
fi
echo "Downloading netatalk-$NETATALK_VERSION to $HOME"
cd $HOME || exit 1
wget -O "netatalk-$NETATALK_VERSION.tar.gz" "https://github.com/rdmark/Netatalk-2.x/archive/refs/tags/netatalk-$NETATALK_VERSION.tar.gz" </dev/null
tar -xzvf netatalk-$NETATALK_VERSION.tar.gz
tar -xzvf "netatalk-$NETATALK_VERSION.tar.gz"
rm "netatalk-$NETATALK_VERSION.tar.gz"
cd "$HOME/Netatalk-2.x-netatalk-$NETATALK_VERSION/contrib/shell_utils" || exit 1
./debian_install.sh -j="${CORES:-1}" -n="$AFP_SHARE_NAME" -p="$AFP_SHARE_PATH" || exit 1
./debian_install.sh -j="${CORES:-1}" -n="$FILE_SHARE_NAME" -p="$FILE_SHARE_PATH" || exit 1
}
# Appends the images dir as a shared Netatalk volume
@ -937,6 +884,65 @@ function installMacproxy {
echo ""
}
# Installs and configures Samba (SMB server)
function installSamba() {
SAMBA_CONFIG_PATH="/etc/samba"
if [ -d "$SAMBA_CONFIG_PATH" ]; then
echo
echo "Samba configuration dir $SAMBA_CONFIG_PATH already exists."
echo "This installation process may overwrite existing binaries and configurations."
echo "No shared files will be deleted, but you may have to manually restore your settings after the installation."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if ! [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
exit 0
fi
fi
if [ ! -d "$FILE_SHARE_PATH" ] && [ -d "$HOME/afpshare" ]; then
echo
echo "File server dir $HOME/afpshare detected. This script will rename it to $FILE_SHARE_PATH."
echo
echo "Do you want to proceed with the installation? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
sudo mv "$HOME/afpshare" "$FILE_SHARE_PATH" || exit 1
else
exit 0
fi
elif [ -d "$FILE_SHARE_PATH" ]; then
echo "Found a $FILE_SHARE_PATH directory; will use it for file sharing."
else
echo "Creating the $FILE_SHARE_PATH directory and granting read/write permissions to all users..."
sudo mkdir -p "$FILE_SHARE_PATH"
sudo chown -R "$USER:$USER" "$FILE_SHARE_PATH"
chmod -Rv 775 "$FILE_SHARE_PATH"
fi
echo ""
echo "Installing dependencies..."
sudo apt-get update || true
sudo apt-get install samba --no-install-recommends --assume-yes </dev/null
echo ""
echo "Modifying $SAMBA_CONFIG_PATH/smb.conf ..."
if [[ `sudo grep -c "server min protocol = NT1" $SAMBA_CONFIG_PATH/smb.conf` -eq 0 ]]; then
# Allow Windows XP clients and earlier to connect to the server
sudo sed -i 's/\[global\]/\[global\]\nserver min protocol = NT1/' "$SAMBA_CONFIG_PATH/smb.conf"
echo "server min prototol = NT1"
fi
if [[ `sudo grep -c "\[Pi File Server\]" $SAMBA_CONFIG_PATH/smb.conf` -eq 0 ]]; then
# Define a shared directory with full read/write privileges, while aggressively hiding dot files
echo -e '\n[Pi File Server]\npath = '"$FILE_SHARE_PATH"'\nbrowseable = yes\nwriteable = yes\nhide dot files = yes\nveto files = /.*/' | sudo tee -a "$SAMBA_CONFIG_PATH/smb.conf"
fi
sudo systemctl restart smbd
echo "Please create a Samba password for user $USER"
sudo smbpasswd -a "$USER"
}
# updates configuration files and installs packages needed for the OLED screen script
function installRaScsiScreen() {
if [[ -f "$SECRET_FILE" && -z "$TOKEN" ]] ; then
@ -1182,6 +1188,8 @@ function runChoice() {
stopOldWebInterface
updateRaScsiGit
installPackages
installHfdisk
fetchHardDiskDrivers
stopRaScsiScreen
stopRaScsi
compileRaScsi
@ -1254,16 +1262,19 @@ function runChoice() {
echo "Installing / Updating RaSCSI OLED Screen - Complete!"
;;
4)
echo "Creating an HFS formatted 600 MiB drive image with LIDO driver"
createDrive600M
echo "Creating an HFS formatted 600 MiB drive image with LIDO driver - Complete!"
echo "Installing / Updating RaSCSI Control Board UI"
echo "This script will make the following changes to your system:"
echo "- Install additional packages with apt-get"
echo "- Add and modify systemd services"
echo "- Stop and disable the RaSCSI OLED service if it is running"
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
sudoCheck
preparePythonCommon
installRaScsiCtrlBoard
showRaScsiCtrlBoardStatus
echo "Installing / Updating RaSCSI Control Board UI - Complete!"
;;
5)
echo "Creating an HFS formatted drive image with LIDO driver"
createDriveCustom
echo "Creating an HFS formatted drive image with LIDO driver - Complete!"
;;
6)
echo "Configuring wired network bridge"
echo "This script will make the following changes to your system:"
echo "- Create a virtual network bridge interface in /etc/network/interfaces.d"
@ -1273,7 +1284,7 @@ function runChoice() {
setupWiredNetworking
echo "Configuring wired network bridge - Complete!"
;;
7)
6)
echo "Configuring wifi network bridge"
echo "This script will make the following changes to your system:"
echo "- Install additional packages with apt-get"
@ -1284,10 +1295,22 @@ function runChoice() {
setupWirelessNetworking
echo "Configuring wifi network bridge - Complete!"
;;
8)
7)
echo "Installing AppleShare File Server"
installNetatalk
echo "Installing AppleShare File Server - Complete!"
;;
8)
echo "Installing SMB File Server"
echo "This script will make the following changes to your system:"
echo " - Install packages with apt-get"
echo " - Enable Samba systemd services"
echo " - Create a directory in the current user's home directory where shared files will be stored"
echo " - Create a Samba user for the current user"
sudoCheck
installSamba
echo "Installing SMB File Server - Complete!"
;;
9)
echo "Installing Web Proxy Server"
echo "This script will make the following changes to your system:"
@ -1327,6 +1350,8 @@ function runChoice() {
createCfgDir
updateRaScsiGit
installPackages
installHfdisk
fetchHardDiskDrivers
preparePythonCommon
cachePipPackages
installRaScsiWebInterface
@ -1352,19 +1377,6 @@ function runChoice() {
echo "Enabling or disabling Web Interface authentication - Complete!"
;;
14)
echo "Installing / Updating RaSCSI Control Board UI"
echo "This script will make the following changes to your system:"
echo "- Install additional packages with apt-get"
echo "- Add and modify systemd services"
echo "- Stop and disable the RaSCSI OLED service if it is running"
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
sudoCheck
preparePythonCommon
installRaScsiCtrlBoard
showRaScsiCtrlBoardStatus
echo "Installing / Updating RaSCSI Control Board UI - Complete!"
;;
15)
shareImagesWithNetatalk
echo "Configuring AppleShare File Server - Complete!"
;;
@ -1382,7 +1394,7 @@ function readChoice() {
choice=-1
until [ $choice -ge "0" ] && [ $choice -le "15" ]; do
echo -n "Enter your choice (0-13) or CTRL-C to exit: "
echo -n "Enter your choice (0-14) or CTRL-C to exit: "
read -r choice
done
@ -1394,27 +1406,24 @@ function showMenu() {
echo ""
echo "Choose among the following options:"
echo "INSTALL/UPDATE RASCSI (${CONNECT_TYPE-FULLSPEC} version)"
echo " 1) install or update RaSCSI Service + Web Interface"
echo " 2) install or update RaSCSI Service"
echo " 3) install or update RaSCSI OLED Screen (requires hardware)"
echo "CREATE HFS FORMATTED (MAC) IMAGE WITH LIDO DRIVERS"
echo "** For the Mac Plus, it's better to create an image through the Web Interface **"
echo " 4) 600 MiB drive (suggested size)"
echo " 5) custom drive size (up to 4000 MiB)"
echo " 1) Install or update RaSCSI Service + Web Interface"
echo " 2) Install or update RaSCSI Service"
echo " 3) Install or update RaSCSI OLED Screen (requires hardware)"
echo " 4) Install or update RaSCSI Control Board UI (requires hardware)"
echo "NETWORK BRIDGE ASSISTANT"
echo " 6) configure network bridge for Ethernet (DHCP)"
echo " 7) configure network bridge for WiFi (static IP + NAT)"
echo " 5) Configure network bridge for Ethernet (DHCP)"
echo " 6) Configure network bridge for WiFi (static IP + NAT)"
echo "INSTALL COMPANION APPS"
echo " 8) install AppleShare File Server (Netatalk)"
echo " 9) install Web Proxy Server (Macproxy)"
echo " 7) Install AppleShare File Server (Netatalk)"
echo " 8) Install SMB File Server (Samba)"
echo " 9) Install Web Proxy Server (Macproxy)"
echo "ADVANCED OPTIONS"
echo " 10) compile and install RaSCSI stand-alone"
echo " 11) configure the RaSCSI Web Interface stand-alone"
echo " 12) enable or disable RaSCSI back-end authentication"
echo " 13) enable or disable RaSCSI Web Interface authentication"
echo " 10) Compile and install RaSCSI stand-alone"
echo " 11) Configure the RaSCSI Web Interface stand-alone"
echo " 12) Enable or disable RaSCSI back-end authentication"
echo " 13) Enable or disable RaSCSI Web Interface authentication"
echo "EXPERIMENTAL FEATURES"
echo " 14) install or update RaSCSI Control Board UI (requires hardware)"
echo " 15) share the images dir over AppleShare (requires Netatalk)"
echo " 14) Share the images dir over AppleShare (requires Netatalk)"
}
# parse arguments passed to the script
@ -1430,8 +1439,8 @@ while [ "$1" != "" ]; do
CONNECT_TYPE=$VALUE
;;
-r | --run_choice)
if ! [[ $VALUE =~ ^[1-9][0-9]?$ && $VALUE -ge 1 && $VALUE -le 15 ]]; then
echo "ERROR: The run choice parameter must have a numeric value between 1 and 15"
if ! [[ $VALUE =~ ^[1-9][0-9]?$ && $VALUE -ge 1 && $VALUE -le 14 ]]; then
echo "ERROR: The run choice parameter must have a numeric value between 1 and 14"
exit 1
fi
RUN_CHOICE=$VALUE

Binary file not shown.

View File

@ -8,11 +8,12 @@ from os import path, walk
from functools import lru_cache
from pathlib import PurePath, Path
from zipfile import ZipFile, is_zipfile
from subprocess import run, CalledProcessError
from subprocess import run, Popen, PIPE, CalledProcessError, TimeoutExpired
from json import dump, load
from shutil import copyfile
from urllib.parse import quote
from tempfile import TemporaryDirectory
from re import search
import requests
@ -366,6 +367,222 @@ class FileCmds:
}
# noinspection PyMethodMayBeStatic
def partition_disk(self, file_name, volume_name, disk_format):
"""
Creates a partition table on an image file.
Takes (str) file_name, (str) volume_name, (str) disk_format as arguments.
disk_format is either HFS or FAT
Returns (dict) with (bool) status, (str) msg
"""
server_info = self.ractl.get_server_info()
full_file_path = Path(server_info["image_dir"]) / file_name
# Inject hfdisk commands to create Drive with correct partitions
# https://www.codesrc.com/mediawiki/index.php/HFSFromScratch
# i initialize partition map
# continue with default first block
# C Create 1st partition with type specified next)
# continue with default
# 32 32 blocks (required for HFS+)
# Driver_Partition Partition Name
# Apple_Driver Partition Type (available types: Apple_Driver,
# Apple_Driver43, Apple_Free, Apple_HFS...)
# C Create 2nd partition with type specified next
# continue with default first block
# continue with default block size (rest of the disk)
# ${volumeName} Partition name provided by user
# Apple_HFS Partition Type
# w Write partition map to disk
# y Confirm partition table
# p Print partition map
if disk_format == "HFS":
partitioning_tool = "hfdisk"
commands = [
"i",
"",
"C",
"",
"32",
"Driver_Partition",
"Apple_Driver",
"C",
"",
"",
volume_name,
"Apple_HFS",
"w",
"y",
"p",
]
# Create a DOS label, primary partition, W95 FAT type
elif disk_format == "FAT":
partitioning_tool = "fdisk"
commands = [
"o",
"n",
"p",
"",
"",
"",
"t",
"b",
"w",
]
try:
process = Popen(
[partitioning_tool, str(full_file_path)],
stdin=PIPE,
stdout=PIPE,
)
for command in commands:
process.stdin.write(bytes(command + "\n", "utf-8"))
process.stdin.flush()
try:
outs, errs = process.communicate(timeout=15)
if outs:
logging.info(str(outs, "utf-8"))
if errs:
logging.error(str(errs, "utf-8"))
if process.returncode:
self.delete_file(Path(file_name))
return {"status": False, "msg": errs}
except TimeoutExpired:
process.kill()
outs, errs = process.communicate()
if outs:
logging.info(str(outs, "utf-8"))
if errs:
logging.error(str(errs, "utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": errs}
except (OSError, IOError) as error:
logging.error(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
return {"status": True, "msg": ""}
# noinspection PyMethodMayBeStatic
def format_hfs(self, file_name, volume_name, driver_path):
"""
Initializes an HFS file system and injects a hard disk driver
Takes (str) file_name, (str) volume_name and (Path) driver_path as arguments.
Returns (dict) with (bool) status, (str) msg
"""
server_info = self.ractl.get_server_info()
full_file_path = Path(server_info["image_dir"]) / file_name
try:
run(
[
"dd",
f"if={driver_path}",
f"of={full_file_path}",
"seek=64",
"count=32",
"bs=512",
"conv=notrunc",
],
capture_output=True,
check=True,
)
except (FileNotFoundError, CalledProcessError) as error:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
try:
process = run(
[
"hformat",
"-l",
volume_name,
str(full_file_path),
"1",
],
capture_output=True,
check=True,
)
logging.info(process.stdout.decode("utf-8"))
except (FileNotFoundError, CalledProcessError) as error:
logging.error(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
return {"status": True, "msg": ""}
# noinspection PyMethodMayBeStatic
def format_fat(self, file_name, volume_name, fat_size):
"""
Initializes a FAT file system
Takes (str) file_name, (str) volume_name and (str) FAT size (12|16|32) as arguments.
Returns (dict) with (bool) status, (str) msg
"""
server_info = self.ractl.get_server_info()
full_file_path = Path(server_info["image_dir"]) / file_name
loopback_device = ""
try:
process = run(
["kpartx", "-av", str(full_file_path)],
capture_output=True,
check=True,
)
logging.info(process.stdout.decode("utf-8"))
if process.returncode == 0:
loopback_device = search(r"(loop\d\D\d)", process.stdout.decode("utf-8")).group(1)
else:
logging.info(process.stdout.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
except (FileNotFoundError, CalledProcessError) as error:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
args = [
"mkfs.fat",
"-v",
"-F",
fat_size,
"-n",
volume_name,
"/dev/mapper/" + loopback_device,
]
try:
process = run(
args,
capture_output=True,
check=True,
)
logging.info(process.stdout.decode("utf-8"))
except (FileNotFoundError, CalledProcessError) as error:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
try:
process = run(
["kpartx", "-dv", str(full_file_path)],
capture_output=True,
check=True,
)
logging.info(process.stdout.decode("utf-8"))
if process.returncode:
logging.info(process.stderr.decode("utf-8"))
logging.warning("Failed to delete loopback device. You may have to do it manually")
except (FileNotFoundError, CalledProcessError) as error:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")}
return {"status": True, "msg": ""}
def download_file_to_iso(self, url, *iso_args):
"""
Takes (str) url and one or more (str) *iso_args
@ -446,9 +663,12 @@ class FileCmds:
headers={"User-Agent": "Mozilla/5.0"},
) as req:
req.raise_for_status()
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
try:
with open(f"{save_dir}/{file_name}", "wb") as download:
for chunk in req.iter_content(chunk_size=8192):
download.write(chunk)
except FileNotFoundError as error:
return {"status": False, "msg": str(error)}
except requests.exceptions.RequestException as error:
logging.warning("Request failed: %s", str(error))
return {"status": False, "msg": str(error)}

View File

@ -8,7 +8,7 @@ import rascsi.common_settings
WEB_DIR = getcwd()
HOME_DIR = "/".join(WEB_DIR.split("/")[0:3])
AFP_DIR = f"{HOME_DIR}/afpshare"
FILE_SERVER_DIR = f"{HOME_DIR}/shared_files"
MAX_FILE_SIZE = getenv("MAX_FILE_SIZE", str(1024 * 1024 * 1024 * 4)) # 4gb

View File

@ -439,7 +439,7 @@
<ul>
<li>{{ _("The largest file size accepted in this form is %(max_file_size)s MiB. Use other file transfer means for larger files.", max_file_size=max_file_size) }}</li>
<li>{{ _("File uploads will progress only if you stay on this page. If you navigate away before the transfer is completed, you will end up with an incomplete file.") }}</li>
<li>{{ _("Install <a href=\"%(url)s\" target=\"_blank\">Netatalk</a> to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
<li>{{ _("Install Netatalk or Samba to use the File Server.") }}</li>
</ul>
</details>
@ -447,8 +447,8 @@
<p>
<label for="upload_destination">{{ _("Target directory:") }}</label>
<select name="destination" id="upload_destination">
<option value="images">Images - {{ env["image_dir"] }}</option>
<option value="afp">AppleShare - {{ AFP_DIR }}</option>
<option value="images">{{ _("Disk Images") }} - {{ env["image_dir"] }}</option>
<option value="file_server">{{ _("File Server") }} - {{ FILE_SERVER_DIR }}</option>
</select>
</p>
</form>
@ -489,15 +489,15 @@
{{ _("Download File from the Web") }}
</summary>
<ul>
<li>{{ _("Install <a href=\"%(url)s\" target=\"_blank\">Netatalk</a> to use the AFP File Server.", url="https://github.com/akuker/RASCSI/wiki/AFP-File-Sharing") }}</li>
<li>{{ _("Install Netatalk or Samba to use the File Server.") }}</li>
</ul>
</details>
<form action="/files/download_url" method="post">
<label for="download_destination">{{ _("Target directory:") }}</label>
<select name="destination" id="download_destination">
<option value="images">Images - {{ env["image_dir"] }}</option>
<option value="afp">AppleShare - {{ AFP_DIR }}</option>
<option value="images">{{ _("Disk Images") }} - {{ env["image_dir"] }}</option>
<option value="file_server">{{ _("File Server") }} - {{ FILE_SERVER_DIR }}</option>
</select>
<label for="download_url">{{ _("URL:") }}</label>
<input name="url" id="download_url" required="" type="url">
@ -560,6 +560,7 @@
</summary>
<ul>
<li>{{ _("Please refer to <a href=\"%(url)s\" target=\"_blank\">wiki documentation</a> to learn more about the supported image file types.", url="https://github.com/akuker/RASCSI/wiki/Supported-Device-Types#image-types") }}</li>
<li>{{ _("It is not recommended to use the Lido hard disk driver with the Macintosh Plus.") }}</li>
</ul>
</details>
@ -587,6 +588,24 @@
</option>
{% endfor %}
</select>
<label for="drive_format">{{ _("Format as:") }}</label>
<select name="drive_format" id="drive_format">
<option value="">
{{ _("None") }}
</option>
<option value="Lido 7.56">
HFS + Lido
</option>
<option value="SpeedTools 3.6">
HFS + SpeedTools
</option>
<option value="FAT16">
FAT16
</option>
<option value="FAT32">
FAT32
</option>
</select>
<input type="submit" value="{{ _("Create") }}">
</form>

View File

@ -59,7 +59,7 @@ from web_utils import (
)
from settings import (
WEB_DIR,
AFP_DIR,
FILE_SERVER_DIR,
MAX_FILE_SIZE,
DEFAULT_CONFIG,
DRIVE_PROPERTIES_FILE,
@ -251,7 +251,7 @@ def index():
drive_properties=format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"]),
RESERVATIONS=RESERVATIONS,
CFG_DIR=CFG_DIR,
AFP_DIR=AFP_DIR,
FILE_SERVER_DIR=FILE_SERVER_DIR,
PROPERTIES_SUFFIX=PROPERTIES_SUFFIX,
ARCHIVE_FILE_SUFFIXES=ARCHIVE_FILE_SUFFIXES,
CONFIG_FILE_SUFFIX=CONFIG_FILE_SUFFIX,
@ -864,8 +864,8 @@ def download_file():
"""
destination = request.form.get("destination")
url = request.form.get("url")
if destination == "afp":
destination_dir = AFP_DIR
if destination == "file_server":
destination_dir = FILE_SERVER_DIR
else:
server_info = ractl_cmd.get_server_info()
destination_dir = server_info["image_dir"]
@ -895,8 +895,8 @@ def upload_file():
return make_response(auth["msg"], 403)
destination = request.form.get("destination")
if destination == "afp":
destination_dir = AFP_DIR
if destination == "file_server":
destination_dir = FILE_SERVER_DIR
else:
server_info = ractl_cmd.get_server_info()
destination_dir = server_info["image_dir"]
@ -913,6 +913,7 @@ def create_file():
size = (int(request.form.get("size")) * 1024 * 1024)
file_type = request.form.get("type")
drive_name = request.form.get("drive_name")
drive_format = request.form.get("drive_format")
safe_path = is_safe_path(file_name)
if not safe_path["status"]:
@ -922,6 +923,68 @@ def create_file():
if not process["status"]:
return response(error=True, message=process["msg"])
message_postfix = ""
# Formatting and injecting driver, if one is choosen
if drive_format:
volume_name = f"HD {size / 1024 / 1024:0.0f}M"
known_formats = [
"Lido 7.56",
"SpeedTools 3.6",
"FAT16",
"FAT32",
]
message_postfix = f" ({drive_format})"
if drive_format not in known_formats:
return response(
error=True,
message=_(
"%(drive_format)s is not a valid hard disk format.",
drive_format=drive_format,
)
)
elif drive_format.startswith("FAT"):
if drive_format == "FAT16":
fat_size = "16"
elif drive_format == "FAT32":
fat_size = "32"
else:
return response(
error=True,
message=_(
"%(drive_format)s is not a valid hard disk format.",
drive_format=drive_format,
)
)
process = file_cmd.partition_disk(full_file_name, volume_name, "FAT")
if not process["status"]:
return response(error=True, message=process["msg"])
process = file_cmd.format_fat(
full_file_name,
# FAT volume labels are max 11 chars
volume_name[:11],
fat_size,
)
if not process["status"]:
return response(error=True, message=process["msg"])
else:
driver_base_path = Path(f"{WEB_DIR}/../../../mac-hard-disk-drivers")
process = file_cmd.partition_disk(full_file_name, volume_name, "HFS")
if not process["status"]:
return response(error=True, message=process["msg"])
process = file_cmd.format_hfs(
full_file_name,
volume_name,
driver_base_path / Path(drive_format.replace(" ", "-") + ".img"),
)
if not process["status"]:
return response(error=True, message=process["msg"])
# Creating the drive properties file, if one is chosen
if drive_name:
properties = get_properties_by_drive_name(
@ -937,15 +1000,20 @@ def create_file():
return response(
status_code=201,
message=_(
"Image file with properties created: %(file_name)s",
"Image file with properties created: %(file_name)s%(drive_format)s",
file_name=full_file_name,
drive_format=message_postfix,
),
image=full_file_name,
)
return response(
status_code=201,
message=_("Image file created: %(file_name)s", file_name=full_file_name),
message=_(
"Image file created: %(file_name)s%(drive_format)s",
file_name=full_file_name,
drive_format=message_postfix,
),
image=full_file_name,
)

View File

@ -65,6 +65,62 @@ def test_create_file_with_properties(http_client, list_files, delete_file):
delete_file(file_name)
# route("/files/create", methods=["POST"])
def test_create_file_and_format_hfs(http_client, list_files, delete_file):
file_prefix = str(uuid.uuid4())
file_name = f"{file_prefix}.hda"
response = http_client.post(
"/files/create",
data={
"file_name": file_prefix,
"type": "hda",
"size": 1,
"drive_format": "Lido 7.56",
},
)
response_data = response.json()
assert response.status_code == 201
assert response_data["status"] == STATUS_SUCCESS
assert response_data["data"]["image"] == file_name
assert response_data["messages"][0]["message"] == f"Image file created: {file_name} (Lido 7.56)"
assert file_name in list_files()
# Cleanup
delete_file(file_name)
# route("/files/create", methods=["POST"])
def test_create_file_and_format_fat(env, http_client, list_files, delete_file):
if env["is_docker"]:
pytest.skip("Test not supported in Docker environment.")
file_prefix = str(uuid.uuid4())
file_name = f"{file_prefix}.hdr"
response = http_client.post(
"/files/create",
data={
"file_name": file_prefix,
"type": "hdr",
"size": 1,
"drive_format": "FAT32",
},
)
response_data = response.json()
assert response.status_code == 201
assert response_data["status"] == STATUS_SUCCESS
assert response_data["data"]["image"] == file_name
assert response_data["messages"][0]["message"] == f"Image file created: {file_name} (FAT32)"
assert file_name in list_files()
# Cleanup
delete_file(file_name)
# route("/files/rename", methods=["POST"])
def test_rename_file(http_client, create_test_image, list_files, delete_file):
original_file = create_test_image(auto_delete=False)

View File

@ -23,7 +23,7 @@ def env(pytestconfig):
"home_dir": home_dir,
"cfg_dir": f"{home_dir}/.config/rascsi",
"images_dir": f"{home_dir}/images",
"afp_dir": f"{home_dir}/afpshare",
"file_server_dir": f"{home_dir}/shared_files",
}