Initial commit.
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
#!/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 (index = 1; index <= 4; index++) {
|
||||
if ($index < 0 || $index > 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
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
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 "${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
|
||||
}
|
||||
|
||||
serviceUser="$(choose_copyparty_user)" || return 1
|
||||
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
|
||||
|
||||
msgbox "Copyparty is configured. For advanced tuning, edit ${copypartyConfigFile} as root."
|
||||
Reference in New Issue
Block a user