517 lines
15 KiB
Bash
517 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
copypartyConfigFile="/etc/copyparty/copyparty.conf"
|
|
copypartyPort="3923"
|
|
|
|
copyparty_installed() {
|
|
pacman -Q copyparty &> /dev/null
|
|
}
|
|
|
|
ufw_installed() {
|
|
pacman -Q ufw &> /dev/null
|
|
}
|
|
|
|
valid_ipv4_subnet() {
|
|
local subnetValue="$1"
|
|
|
|
[[ "$subnetValue" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$ ]] || return 1
|
|
awk -F '[./]' '
|
|
{
|
|
for (octetIndex = 1; octetIndex <= 4; octetIndex++) {
|
|
if ($octetIndex < 0 || $octetIndex > 255) {
|
|
exit 1
|
|
}
|
|
}
|
|
exit 0
|
|
}
|
|
' <<< "$subnetValue"
|
|
}
|
|
|
|
detect_private_subnets() {
|
|
ip -o -4 addr show up scope global | awk '
|
|
{
|
|
split($4, parts, "/")
|
|
split(parts[1], octets, ".")
|
|
|
|
if (octets[1] == 10) {
|
|
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
|
|
} else if (octets[1] == 192 && octets[2] == 168) {
|
|
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
|
|
} else if (octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31) {
|
|
subnet = octets[1] "." octets[2] "." octets[3] ".0/24"
|
|
} else {
|
|
next
|
|
}
|
|
|
|
if (!seen[subnet]++) {
|
|
print subnet
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
detect_first_private_ipv4() {
|
|
ip -o -4 addr show up scope global | awk '
|
|
{
|
|
split($4, parts, "/")
|
|
split(parts[1], octets, ".")
|
|
|
|
if (octets[1] == 10 ||
|
|
(octets[1] == 192 && octets[2] == 168) ||
|
|
(octets[1] == 172 && octets[2] >= 16 && octets[2] <= 31)) {
|
|
print parts[1]
|
|
exit 0
|
|
}
|
|
}
|
|
'
|
|
}
|
|
|
|
show_copyparty_address() {
|
|
local lanAddress=""
|
|
|
|
if ! copyparty_installed; then
|
|
msgbox "Copyparty is not installed."
|
|
return 1
|
|
fi
|
|
|
|
lanAddress="$(detect_first_private_ipv4)"
|
|
if [[ -z "$lanAddress" ]]; then
|
|
msgbox "Copyparty is installed, but no private IPv4 address could be detected automatically."
|
|
return 1
|
|
fi
|
|
|
|
msgbox "Copyparty should be available at http://${lanAddress}:${copypartyPort}"
|
|
return 0
|
|
}
|
|
|
|
choose_lan_subnet() {
|
|
local detectedSubnet=""
|
|
local subnetChoice=""
|
|
|
|
detectedSubnet="$(detect_private_subnets | head -n 1)"
|
|
subnetChoice="$(inputbox "Confirm the LAN subnet for Copyparty access." "${detectedSubnet:-192.168.1.0/24}")" || return 1
|
|
if valid_ipv4_subnet "$subnetChoice"; then
|
|
printf '%s\n' "$subnetChoice"
|
|
return 0
|
|
fi
|
|
|
|
msgbox "Enter a valid IPv4 subnet such as 192.168.1.0/24."
|
|
return 1
|
|
}
|
|
|
|
install_copyparty() {
|
|
local packageList=(
|
|
copyparty
|
|
ffmpeg
|
|
cfssl
|
|
python-mutagen
|
|
python-pillow
|
|
python-pyvips
|
|
libkeyfinder
|
|
python-pyopenssl
|
|
python-pyzmq
|
|
python-argon2-cffi
|
|
)
|
|
|
|
if copyparty_installed; then
|
|
return 0
|
|
fi
|
|
|
|
if ! install_package "${packageList[@]}"; then
|
|
msgbox "Failed to install Copyparty and its optional feature packages."
|
|
return 1
|
|
fi
|
|
|
|
msgbox "Copyparty and its optional feature packages are installed."
|
|
return 0
|
|
}
|
|
|
|
list_local_users() {
|
|
awk -F ':' '
|
|
$3 >= 1000 && $3 < 60000 && $7 !~ /(nologin|false)$/ {
|
|
print $1
|
|
}
|
|
' /etc/passwd | sort
|
|
}
|
|
|
|
choose_copyparty_user() {
|
|
local userEntries=()
|
|
local userName=""
|
|
local selectedUser=""
|
|
|
|
while IFS= read -r userName; do
|
|
[[ -n "$userName" ]] && userEntries+=("$userName")
|
|
done < <(list_local_users)
|
|
|
|
if [[ "${#userEntries[@]}" -eq 0 ]]; then
|
|
msgbox "No regular local users were found."
|
|
return 1
|
|
fi
|
|
|
|
selectedUser="$(menulist --prompt "Please select the user Copyparty should be run as." "${userEntries[@]}")" || return 1
|
|
printf '%s\n' "$selectedUser"
|
|
return 0
|
|
}
|
|
|
|
account_name_exists() {
|
|
local accountsFile="$1"
|
|
local username="$2"
|
|
|
|
awk -F ':' -v username="$username" '
|
|
/^[[:space:]]+[A-Za-z0-9._-]+:/ {
|
|
accountName = $1
|
|
gsub(/^[[:space:]]+/, "", accountName)
|
|
if (accountName == username) {
|
|
exit 0
|
|
}
|
|
}
|
|
END {
|
|
exit 1
|
|
}
|
|
' "$accountsFile"
|
|
}
|
|
|
|
collect_copyparty_accounts() {
|
|
local tempAccountsFile=""
|
|
local username=""
|
|
local password=""
|
|
local addAnother="No"
|
|
|
|
tempAccountsFile="$(mktemp)"
|
|
while true; do
|
|
username="$(inputbox "Enter a Copyparty username.")" || {
|
|
rm -f "$tempAccountsFile"
|
|
return 1
|
|
}
|
|
if [[ -z "$username" || "$username" =~ [[:space:]:] ]]; then
|
|
msgbox "Usernames cannot be blank and cannot contain spaces or colons."
|
|
continue
|
|
fi
|
|
if account_name_exists "$tempAccountsFile" "$username"; then
|
|
msgbox "That Copyparty username already exists in this configuration."
|
|
continue
|
|
fi
|
|
|
|
password="$(passwordbox "Enter the password for ${username}.")" || {
|
|
rm -f "$tempAccountsFile"
|
|
return 1
|
|
}
|
|
if [[ -z "$password" ]]; then
|
|
msgbox "Passwords cannot be blank."
|
|
continue
|
|
fi
|
|
|
|
printf ' %s: %s\n' "$username" "$password" >> "$tempAccountsFile"
|
|
addAnother="$(yesno "Add another Copyparty account?")"
|
|
if [[ "$addAnother" != "Yes" ]]; then
|
|
break
|
|
fi
|
|
done
|
|
|
|
printf '%s\n' "$tempAccountsFile"
|
|
return 0
|
|
}
|
|
|
|
choose_share_mode() {
|
|
local shareMode=""
|
|
|
|
shareMode="$(menulist "Private read-write" "Private read-only" "Public read-only")" || return 1
|
|
printf '%s\n' "$shareMode"
|
|
return 0
|
|
}
|
|
|
|
append_share_config() {
|
|
local outputFile="$1"
|
|
local sharePath="$2"
|
|
local urlPath="$3"
|
|
local shareMode="$4"
|
|
local usernamesCsv="$5"
|
|
|
|
{
|
|
printf '\n[%s]\n' "$urlPath"
|
|
printf ' %s\n' "$sharePath"
|
|
printf ' accs:\n'
|
|
case "$shareMode" in
|
|
"Private read-write")
|
|
printf ' rwmd: %s\n' "$usernamesCsv"
|
|
printf ' flags:\n'
|
|
printf ' daw\n'
|
|
;;
|
|
"Private read-only")
|
|
printf ' r: %s\n' "$usernamesCsv"
|
|
;;
|
|
"Public read-only")
|
|
printf ' r: *\n'
|
|
;;
|
|
esac
|
|
} >> "$outputFile"
|
|
}
|
|
|
|
share_url_exists() {
|
|
local sharesFile="$1"
|
|
local urlPath="$2"
|
|
|
|
awk -v urlPath="$urlPath" '
|
|
/^\[[^]]+\]$/ {
|
|
sectionName = $0
|
|
sub(/^\[/, "", sectionName)
|
|
sub(/\]$/, "", sectionName)
|
|
if (sectionName == urlPath) {
|
|
exit 0
|
|
}
|
|
}
|
|
END {
|
|
exit 1
|
|
}
|
|
' "$sharesFile"
|
|
}
|
|
|
|
service_user_can_read_share() {
|
|
local serviceUser="$1"
|
|
local sharePath="$2"
|
|
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
sudo "${sudoFlags[@]}" -u "$serviceUser" test -r "$sharePath" &&
|
|
sudo "${sudoFlags[@]}" -u "$serviceUser" test -x "$sharePath"
|
|
}
|
|
|
|
service_user_can_write_share() {
|
|
local serviceUser="$1"
|
|
local sharePath="$2"
|
|
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
sudo "${sudoFlags[@]}" -u "$serviceUser" test -w "$sharePath" &&
|
|
sudo "${sudoFlags[@]}" -u "$serviceUser" test -x "$sharePath"
|
|
}
|
|
|
|
collect_copyparty_shares() {
|
|
local usernamesCsv="$1"
|
|
local serviceUser="$2"
|
|
local tempSharesFile=""
|
|
local sharePath=""
|
|
local urlPath=""
|
|
local shareMode=""
|
|
local addAnother="No"
|
|
|
|
tempSharesFile="$(mktemp)"
|
|
while true; do
|
|
sharePath="$(inputbox "Enter the local path to share.")" || {
|
|
rm -f "$tempSharesFile"
|
|
return 1
|
|
}
|
|
if [[ -z "$sharePath" ]]; then
|
|
msgbox "A local path is required."
|
|
continue
|
|
fi
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
if ! sudo "${sudoFlags[@]}" test -d "$sharePath"; then
|
|
msgbox "The path ${sharePath} does not exist or is not a directory."
|
|
continue
|
|
fi
|
|
if ! service_user_can_read_share "$serviceUser" "$sharePath"; then
|
|
msgbox "The selected service user ${serviceUser} cannot read ${sharePath}. Adjust permissions or choose another path."
|
|
continue
|
|
fi
|
|
|
|
urlPath="$(inputbox "Enter the Copyparty URL path for this share, for example /files." "/files")" || {
|
|
rm -f "$tempSharesFile"
|
|
return 1
|
|
}
|
|
if [[ ! "$urlPath" =~ ^/[^[:space:]]*$ ]]; then
|
|
msgbox "Enter a URL path that starts with / and contains no spaces."
|
|
continue
|
|
fi
|
|
if share_url_exists "$tempSharesFile" "$urlPath"; then
|
|
msgbox "That Copyparty URL path is already defined in this configuration."
|
|
continue
|
|
fi
|
|
|
|
shareMode="$(choose_share_mode)" || {
|
|
rm -f "$tempSharesFile"
|
|
return 1
|
|
}
|
|
if [[ "$shareMode" == "Private read-write" ]] &&
|
|
! service_user_can_write_share "$serviceUser" "$sharePath"; then
|
|
msgbox "The selected service user ${serviceUser} cannot write to ${sharePath}. Choose another path or adjust permissions."
|
|
continue
|
|
fi
|
|
append_share_config "$tempSharesFile" "$sharePath" "$urlPath" "$shareMode" "$usernamesCsv"
|
|
addAnother="$(yesno "Add another Copyparty share?")"
|
|
if [[ "$addAnother" != "Yes" ]]; then
|
|
break
|
|
fi
|
|
done
|
|
|
|
printf '%s\n' "$tempSharesFile"
|
|
return 0
|
|
}
|
|
|
|
join_account_names() {
|
|
local accountsFile="$1"
|
|
|
|
awk -F ':' '
|
|
BEGIN {
|
|
first = 1
|
|
}
|
|
/^[[:space:]]+[A-Za-z0-9._-]+:/ {
|
|
gsub(/^[[:space:]]+/, "", $1)
|
|
if (!first) {
|
|
printf ", "
|
|
}
|
|
printf "%s", $1
|
|
first = 0
|
|
}
|
|
END {
|
|
printf "\n"
|
|
}
|
|
' "$accountsFile"
|
|
}
|
|
|
|
write_copyparty_config() {
|
|
local serviceUser="$1"
|
|
local accountsFile="$2"
|
|
local sharesFile="$3"
|
|
local tempConfigFile=""
|
|
local serviceGroup=""
|
|
|
|
tempConfigFile="$(mktemp)"
|
|
serviceGroup="$(id -gn "$serviceUser")"
|
|
{
|
|
printf '[global]\n'
|
|
printf ' i: 0.0.0.0\n'
|
|
printf ' p: %s\n' "$copypartyPort"
|
|
printf ' e2dsa\n'
|
|
printf '\n[accounts]\n'
|
|
cat "$accountsFile"
|
|
cat "$sharesFile"
|
|
} > "$tempConfigFile"
|
|
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
if ! sudo "${sudoFlags[@]}" install -d -m 755 /etc/copyparty; then
|
|
rm -f "$tempConfigFile"
|
|
msgbox "Failed to create /etc/copyparty."
|
|
return 1
|
|
fi
|
|
|
|
# shellcheck disable=SC2154
|
|
if ! sudo "${sudoFlags[@]}" install -o root -g "$serviceGroup" -m 640 "$tempConfigFile" "$copypartyConfigFile"; then
|
|
rm -f "$tempConfigFile"
|
|
msgbox "Failed to write ${copypartyConfigFile}."
|
|
return 1
|
|
fi
|
|
|
|
rm -f "$tempConfigFile"
|
|
return 0
|
|
}
|
|
|
|
enable_copyparty_service() {
|
|
local serviceUser="$1"
|
|
local unitName="copyparty@${serviceUser}"
|
|
local configuredUnit=""
|
|
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
while IFS= read -r configuredUnit; do
|
|
[[ -z "$configuredUnit" || "$configuredUnit" == "$unitName" ]] && continue
|
|
sudo "${sudoFlags[@]}" systemctl disable --now "$configuredUnit" &> /dev/null || true
|
|
done < <(
|
|
sudo "${sudoFlags[@]}" find /etc/systemd/system -type l -name 'copyparty@*.service' -printf '%f\n' 2> /dev/null | sort -u
|
|
)
|
|
|
|
if ! sudo "${sudoFlags[@]}" systemctl enable "$unitName"; then
|
|
msgbox "Copyparty was configured, but the service failed to enable."
|
|
return 1
|
|
fi
|
|
|
|
if sudo "${sudoFlags[@]}" systemctl is-active --quiet "$unitName"; then
|
|
if ! sudo "${sudoFlags[@]}" systemctl restart "$unitName"; then
|
|
msgbox "Copyparty was configured, but the running service failed to restart."
|
|
return 1
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
if ! sudo "${sudoFlags[@]}" systemctl start "$unitName"; then
|
|
msgbox "Copyparty was configured, but the service failed to start."
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
configure_copyparty_firewall() {
|
|
local lanSubnet=""
|
|
|
|
if ! ufw_installed; then
|
|
msgbox "ufw is not installed, so Copyparty will listen on ${copypartyPort} without helper-managed LAN restrictions. Install and configure ufw if you need local-only enforcement."
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$(yesno "ufw is installed. Allow LAN-only access to Copyparty on port ${copypartyPort}?")" != "Yes" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
lanSubnet="$(choose_lan_subnet)" || return 1
|
|
# `sudoFlags` is initialized by the main launcher before sourcing this file.
|
|
# shellcheck disable=SC2154
|
|
if ! sudo "${sudoFlags[@]}" ufw allow from "$lanSubnet" to any port "$copypartyPort" proto tcp; then
|
|
msgbox "Failed to allow Copyparty access for ${lanSubnet}."
|
|
return 1
|
|
fi
|
|
|
|
msgbox "Copyparty is allowed on ${copypartyPort}/tcp for ${lanSubnet}."
|
|
return 0
|
|
}
|
|
|
|
install_copyparty_flow() {
|
|
local serviceUser=""
|
|
local unitName=""
|
|
local accountsFile=""
|
|
local usernamesCsv=""
|
|
local sharesFile=""
|
|
local lanAddress=""
|
|
|
|
serviceUser="$(choose_copyparty_user)" || return 1
|
|
unitName="copyparty@${serviceUser}.service"
|
|
install_copyparty || return 1
|
|
accountsFile="$(collect_copyparty_accounts)" || return 1
|
|
usernamesCsv="$(join_account_names "$accountsFile")"
|
|
sharesFile="$(collect_copyparty_shares "$usernamesCsv" "$serviceUser")" || {
|
|
rm -f "$accountsFile"
|
|
return 1
|
|
}
|
|
|
|
write_copyparty_config "$serviceUser" "$accountsFile" "$sharesFile" || {
|
|
rm -f "$accountsFile" "$sharesFile"
|
|
return 1
|
|
}
|
|
rm -f "$accountsFile" "$sharesFile"
|
|
|
|
enable_copyparty_service "$serviceUser" || return 1
|
|
configure_copyparty_firewall || return 1
|
|
|
|
lanAddress="$(detect_first_private_ipv4)"
|
|
if [[ -n "$lanAddress" ]]; then
|
|
msgbox "Copyparty is configured. Enabled unit: ${unitName}. It should now be available at http://${lanAddress}:${copypartyPort}. For advanced tuning, edit ${copypartyConfigFile} as root."
|
|
else
|
|
msgbox "Copyparty is configured. Enabled unit: ${unitName}. For advanced tuning, edit ${copypartyConfigFile} as root."
|
|
fi
|
|
}
|
|
|
|
case "${1:-install}" in
|
|
install)
|
|
install_copyparty_flow
|
|
;;
|
|
show-address)
|
|
show_copyparty_address
|
|
;;
|
|
*)
|
|
msgbox "Unknown Copyparty action: ${1}"
|
|
return 1
|
|
;;
|
|
esac
|