mirror of
https://github.com/akuker/RASCSI.git
synced 2024-12-21 08:29:59 +00:00
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:
parent
9a4f433baf
commit
85edd50047
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
|
||||
|
@ -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
|
||||
|
283
easyinstall.sh
283
easyinstall.sh
@ -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
|
||||
|
BIN
lido-driver.img
BIN
lido-driver.img
Binary file not shown.
@ -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)}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user