forked from finn/tinyboard
Compare commits
18 Commits
rclone-twe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7c5d2bf8d | ||
|
|
85def22fca | ||
|
|
80d5f1d1fd | ||
|
|
56e0fc38c0 | ||
|
|
1c6e12e2d6 | ||
|
|
40d24158b6 | ||
|
|
5bc33b28f4 | ||
|
|
21a1c7e922 | ||
|
|
4586a0f598 | ||
|
|
2999c464fa | ||
|
|
dfa3c1ce6d | ||
|
|
9dc2b221d3 | ||
|
|
89e84c41c1 | ||
|
|
2d2b19b2db | ||
|
|
78d4373c0d | ||
|
|
132d15357c | ||
|
|
8acfc3269a | ||
|
|
ad15498bb9 |
@@ -48,6 +48,22 @@ if [ "$IS_SPOKE" = false ] && [ "$IS_HUB" = false ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_disk() {
|
||||
header "Disk Space"
|
||||
while IFS= read -r line; do
|
||||
local pct mount
|
||||
pct=$(echo "$line" | awk '{print $5}' | tr -d '%')
|
||||
mount=$(echo "$line" | awk '{print $6}')
|
||||
if [ "$pct" -ge 90 ]; then
|
||||
fail "Disk usage at ${pct}% on $mount — critically low"
|
||||
elif [ "$pct" -ge 80 ]; then
|
||||
warn "Disk usage at ${pct}% on $mount — getting full"
|
||||
else
|
||||
ok "Disk usage at ${pct}% on $mount"
|
||||
fi
|
||||
done < <(df -h | awk 'NR>1' | grep -v ' /dev' | grep -v ' /sys' | grep -v ' /proc' | grep -v ' /run' | grep -v ' /snap' | grep -v 'overlay2' | grep -v 'docker')
|
||||
}
|
||||
|
||||
check_common() {
|
||||
header "System"
|
||||
|
||||
@@ -202,6 +218,7 @@ check_hub() {
|
||||
}
|
||||
|
||||
check_common
|
||||
check_disk
|
||||
|
||||
if [ "$IS_SPOKE" = true ]; then
|
||||
check_spoke
|
||||
|
||||
@@ -13,15 +13,8 @@ NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() {
|
||||
echo -e "${RED}[ERROR]${NC} $*" >&2
|
||||
exit 1
|
||||
}
|
||||
header() {
|
||||
echo -e "\n${CYAN}══════════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} $*${NC}"
|
||||
echo -e "${CYAN}══════════════════════════════════════════${NC}"
|
||||
}
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
@@ -91,9 +84,9 @@ KEYSCAN=$(ssh-keyscan -p "$TUNNEL_PORT" -H localhost 2>/dev/null)
|
||||
while IFS= read -r KEYSCAN_LINE; do
|
||||
KEYSCAN_KEY=$(echo "$KEYSCAN_LINE" | awk '{print $2, $3}')
|
||||
if ! grep -qF "$KEYSCAN_KEY" "$SSH_DIR/known_hosts" 2>/dev/null; then
|
||||
echo "$KEYSCAN_LINE" >>"$SSH_DIR/known_hosts"
|
||||
echo "$KEYSCAN_LINE" >> "$SSH_DIR/known_hosts"
|
||||
fi
|
||||
done <<<"$KEYSCAN"
|
||||
done <<< "$KEYSCAN"
|
||||
|
||||
header "Generating Hub-to-Spoke Access Key"
|
||||
if [ -f "$KEY_PATH" ]; then
|
||||
@@ -131,7 +124,7 @@ header "Adding rclone Remote"
|
||||
if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
|
||||
warn "Remote [${SPOKE_NAME}-remote] already exists in $RCLONE_CONF, skipping."
|
||||
else
|
||||
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >>"$RCLONE_CONF"
|
||||
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF"
|
||||
python3 - "$RCLONE_CONF" "$SPOKE_NAME" "$TUNNEL_PORT" "$KEY_PATH" <<'PYEOF'
|
||||
import sys
|
||||
path, name, port, key = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||
@@ -162,10 +155,7 @@ if [[ "${ADD_UNION,,}" == "y" ]]; then
|
||||
1) UPSTREAM_TAG=":ro" ;;
|
||||
2) UPSTREAM_TAG=":nc" ;;
|
||||
3) UPSTREAM_TAG=":writeback" ;;
|
||||
*)
|
||||
warn "Invalid choice, defaulting to full read/write."
|
||||
UPSTREAM_TAG=""
|
||||
;;
|
||||
*) warn "Invalid choice, defaulting to full read/write."; UPSTREAM_TAG="" ;;
|
||||
esac
|
||||
if [ -n "$UNION_PATH" ]; then
|
||||
UPSTREAM="${SPOKE_NAME}-remote:${UNION_PATH}${UPSTREAM_TAG}"
|
||||
@@ -173,8 +163,7 @@ if [[ "${ADD_UNION,,}" == "y" ]]; then
|
||||
UPSTREAM="${SPOKE_NAME}-remote:${UPSTREAM_TAG}"
|
||||
fi
|
||||
if grep -q "^\[${UNION_NAME}\]" "$RCLONE_CONF" 2>/dev/null; then
|
||||
ALREADY=$(
|
||||
python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF2'
|
||||
ALREADY=$(python3 - "$RCLONE_CONF" "$UNION_NAME" "${SPOKE_NAME}-remote:" <<'PYEOF2'
|
||||
import sys
|
||||
path, section, prefix = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
with open(path) as f:
|
||||
@@ -190,7 +179,7 @@ for line in lines:
|
||||
sys.exit(0)
|
||||
print("no")
|
||||
PYEOF2
|
||||
)
|
||||
)
|
||||
if [ "$ALREADY" = "yes" ]; then
|
||||
warn "Upstream for ${SPOKE_NAME}-remote already in union remote [${UNION_NAME}], skipping."
|
||||
else
|
||||
@@ -215,8 +204,8 @@ PYEOF2
|
||||
info "Added '$UPSTREAM' to union remote [${UNION_NAME}]."
|
||||
fi
|
||||
else
|
||||
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >>"$RCLONE_CONF"
|
||||
printf '\n[%s]\ntype = union\nupstreams = %s\n' "$UNION_NAME" "$UPSTREAM" >>"$RCLONE_CONF"
|
||||
[ -s "$RCLONE_CONF" ] && tail -c1 "$RCLONE_CONF" | grep -qv $'\n' && echo "" >> "$RCLONE_CONF"
|
||||
printf '\n[%s]\ntype = union\nupstreams = %s\n' "$UNION_NAME" "$UPSTREAM" >> "$RCLONE_CONF"
|
||||
info "Union remote [${UNION_NAME}] created with upstream '$UPSTREAM'."
|
||||
fi
|
||||
fi
|
||||
@@ -234,15 +223,14 @@ MOUNT_POINT="${HOME}/mnt/${SPOKE_NAME}"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
if grep -q "^${SPOKE_NAME} " "$REGISTRY" 2>/dev/null; then
|
||||
warn "$SPOKE_NAME already in registry, updating."
|
||||
grep -v "^${SPOKE_NAME} " "$REGISTRY" >"${REGISTRY}.tmp" 2>/dev/null || true
|
||||
grep -v "^${SPOKE_NAME} " "$REGISTRY" > "${REGISTRY}.tmp" 2>/dev/null || true
|
||||
mv "${REGISTRY}.tmp" "$REGISTRY"
|
||||
fi
|
||||
echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >>"$REGISTRY"
|
||||
echo "${SPOKE_NAME} ${TUNNEL_PORT} ${KEY_PATH} ${MOUNT_POINT}" >> "$REGISTRY"
|
||||
info "$SPOKE_NAME registered."
|
||||
|
||||
header "Setting Up Auto-Mount"
|
||||
#MOUNT_CMD="rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode writes --allow-other --daemon"
|
||||
MOUNT_CMD="sleep 55 && rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode full --vfs-cache-max-size 2G --vfs-read-ahead 256M --allow-other --daemon"
|
||||
MOUNT_CMD="/usr/bin/rclone mount ${SPOKE_NAME}-remote: ${MOUNT_POINT} --config ${HOME}/.config/rclone/rclone.conf --vfs-cache-mode writes --allow-other --allow-non-empty --daemon"
|
||||
CRON_ENTRY="@reboot ${MOUNT_CMD}"
|
||||
EXISTING=$(crontab -l 2>/dev/null || true)
|
||||
if echo "$EXISTING" | grep -qF "${SPOKE_NAME}-remote:"; then
|
||||
@@ -250,12 +238,9 @@ if echo "$EXISTING" | grep -qF "${SPOKE_NAME}-remote:"; then
|
||||
else
|
||||
CRONTAB_BACKUP="${HOME}/.config/tinyboard/crontab.$(date +%Y%m%d%H%M%S)"
|
||||
mkdir -p "$(dirname "$CRONTAB_BACKUP")"
|
||||
echo "$EXISTING" >"$CRONTAB_BACKUP"
|
||||
echo "$EXISTING" > "$CRONTAB_BACKUP"
|
||||
info "Crontab backed up to $CRONTAB_BACKUP"
|
||||
{
|
||||
echo "$EXISTING"
|
||||
echo "$CRON_ENTRY"
|
||||
} | crontab -
|
||||
{ echo "$EXISTING"; echo "$CRON_ENTRY"; } | crontab -
|
||||
info "Auto-mount crontab entry added for ${SPOKE_NAME}."
|
||||
fi
|
||||
info "Starting mount now..."
|
||||
|
||||
363
hub/setup-opds.sh
Executable file
363
hub/setup-opds.sh
Executable file
@@ -0,0 +1,363 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run as root"
|
||||
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
warn "Docker is not installed."
|
||||
read -rp "Install Docker now? [Y/n]: " INSTALL_DOCKER
|
||||
INSTALL_DOCKER="${INSTALL_DOCKER:-y}"
|
||||
if [[ "${INSTALL_DOCKER,,}" == "y" ]]; then
|
||||
header "Installing Docker"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
apt-get update -q
|
||||
apt-get install -y -q docker.io docker-cli docker-compose
|
||||
else
|
||||
curl -fsSL https://get.docker.com | bash
|
||||
fi
|
||||
command -v docker >/dev/null 2>&1 || die "Docker installation failed."
|
||||
info "Docker installed."
|
||||
else
|
||||
die "Docker is required. Aborting."
|
||||
fi
|
||||
fi
|
||||
|
||||
header "TinyBoard OPDS Setup"
|
||||
echo ""
|
||||
|
||||
EXISTING_SERVER=""
|
||||
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qE '^(dir2opds|stump)$'; then
|
||||
EXISTING_SERVER=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -E '^(dir2opds|stump)$' | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$EXISTING_SERVER" ]; then
|
||||
echo "An existing OPDS server was found: ${EXISTING_SERVER}"
|
||||
echo ""
|
||||
echo " 1) Reconfigure — update books path, domain, SSL, or auth"
|
||||
echo " 2) Fresh install — remove existing and start over"
|
||||
echo " q) Quit"
|
||||
echo ""
|
||||
read -rp "Choose [1/2/q]: " RECONF_CHOICE
|
||||
case "$RECONF_CHOICE" in
|
||||
1) info "Reconfiguring existing setup..." ;;
|
||||
2)
|
||||
docker rm -f dir2opds stump caddy-opds 2>/dev/null || true
|
||||
info "Existing containers removed."
|
||||
;;
|
||||
q|Q) exit 0 ;;
|
||||
*) die "Invalid choice." ;;
|
||||
esac
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Choose an OPDS server:"
|
||||
echo " 1) dir2opds — lightweight, no database, serves files directly from a folder"
|
||||
echo " 2) Stump — full-featured book/comic server with web UI and OPDS support"
|
||||
echo ""
|
||||
read -rp "Choose [1/2]: " OPDS_CHOICE
|
||||
|
||||
case "$OPDS_CHOICE" in
|
||||
1)
|
||||
OPDS_SERVER="dir2opds"
|
||||
OPDS_IMAGE="ghcr.io/dubyte/dir2opds:latest"
|
||||
OPDS_INTERNAL_PORT="8080"
|
||||
;;
|
||||
2)
|
||||
OPDS_SERVER="stump"
|
||||
OPDS_IMAGE="aaronleopold/stump"
|
||||
OPDS_INTERNAL_PORT="10801"
|
||||
;;
|
||||
*)
|
||||
die "Invalid choice."
|
||||
;;
|
||||
esac
|
||||
|
||||
info "Selected: $OPDS_SERVER"
|
||||
echo ""
|
||||
|
||||
read -rp "OPDS domain (e.g. opds.yourdomain.com): " OPDS_DOMAIN
|
||||
[ -n "$OPDS_DOMAIN" ] || die "Domain cannot be empty"
|
||||
|
||||
read -rp "Hub user [armbian]: " HUB_USER
|
||||
HUB_USER="${HUB_USER:-armbian}"
|
||||
HUB_HOME="/home/$HUB_USER"
|
||||
|
||||
MOUNT_BASE="${HUB_HOME}/mnt"
|
||||
AVAILABLE_MOUNTS=()
|
||||
if [ -d "$MOUNT_BASE" ]; then
|
||||
while IFS= read -r mountdir; do
|
||||
AVAILABLE_MOUNTS+=("$mountdir")
|
||||
done < <(find "$MOUNT_BASE" -mindepth 1 -maxdepth 1 -type d | sort)
|
||||
fi
|
||||
|
||||
if [ ${#AVAILABLE_MOUNTS[@]} -gt 0 ]; then
|
||||
echo "Available spoke mounts:"
|
||||
for i in "${!AVAILABLE_MOUNTS[@]}"; do
|
||||
echo " $i) ${AVAILABLE_MOUNTS[$i]}"
|
||||
done
|
||||
echo " m) Enter path manually"
|
||||
echo ""
|
||||
read -rp "Choose a mount [0]: " MOUNT_CHOICE
|
||||
MOUNT_CHOICE="${MOUNT_CHOICE:-0}"
|
||||
if [[ "$MOUNT_CHOICE" == "m" ]]; then
|
||||
read -rp "Path to books directory: " BOOKS_PATH
|
||||
elif [[ "$MOUNT_CHOICE" =~ ^[0-9]+$ ]] && [ "$MOUNT_CHOICE" -lt "${#AVAILABLE_MOUNTS[@]}" ]; then
|
||||
BASE="${AVAILABLE_MOUNTS[$MOUNT_CHOICE]}"
|
||||
read -rp "Subdirectory within ${BASE} (leave blank for root): " SUBDIR
|
||||
if [ -n "$SUBDIR" ]; then
|
||||
BOOKS_PATH="${BASE}/${SUBDIR}"
|
||||
else
|
||||
BOOKS_PATH="$BASE"
|
||||
fi
|
||||
else
|
||||
die "Invalid choice."
|
||||
fi
|
||||
else
|
||||
warn "No spoke mounts found in ${MOUNT_BASE}."
|
||||
read -rp "Path to books directory: " BOOKS_PATH
|
||||
fi
|
||||
|
||||
[ -n "$BOOKS_PATH" ] || die "Books path cannot be empty."
|
||||
[ -d "$BOOKS_PATH" ] || die "Books directory not found: $BOOKS_PATH"
|
||||
|
||||
echo ""
|
||||
echo "SSL certificate management:"
|
||||
echo " 1) Automatic — Caddy obtains and renews certs from Let's Encrypt (recommended)"
|
||||
echo " 2) Manual — provide paths to existing certificate files"
|
||||
echo ""
|
||||
read -rp "Choose [1/2]: " SSL_CHOICE
|
||||
|
||||
AUTO_HTTPS=true
|
||||
CERT_PATH=""
|
||||
KEY_PATH=""
|
||||
|
||||
case "$SSL_CHOICE" in
|
||||
1)
|
||||
AUTO_HTTPS=true
|
||||
info "Automatic HTTPS selected — DNS must point ${OPDS_DOMAIN} to this server."
|
||||
;;
|
||||
2)
|
||||
AUTO_HTTPS=false
|
||||
read -rp "Path to fullchain.pem: " CERT_PATH
|
||||
[ -f "$CERT_PATH" ] || die "Certificate file not found: $CERT_PATH"
|
||||
read -rp "Path to privkey.pem: " KEY_PATH
|
||||
[ -f "$KEY_PATH" ] || die "Key file not found: $KEY_PATH"
|
||||
;;
|
||||
*)
|
||||
die "Invalid choice."
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
read -rp "Protect with a password? [y/N]: " USE_AUTH
|
||||
USE_AUTH="${USE_AUTH:-n}"
|
||||
OPDS_USER=""
|
||||
OPDS_PASS=""
|
||||
HASHED_PASS=""
|
||||
if [[ "${USE_AUTH,,}" == "y" ]]; then
|
||||
read -rp "Username [opds]: " OPDS_USER
|
||||
OPDS_USER="${OPDS_USER:-opds}"
|
||||
read -rsp "Password: " OPDS_PASS
|
||||
echo ""
|
||||
[ -n "$OPDS_PASS" ] || die "Password cannot be empty"
|
||||
fi
|
||||
|
||||
OPDS_DIR="${HUB_HOME}/opds"
|
||||
mkdir -p "$OPDS_DIR"
|
||||
chown "$HUB_USER":"$HUB_USER" "$OPDS_DIR"
|
||||
|
||||
header "Creating Docker Network"
|
||||
docker network create opds-net 2>/dev/null || info "Network opds-net already exists."
|
||||
|
||||
header "Starting $OPDS_SERVER"
|
||||
docker rm -f "$OPDS_SERVER" 2>/dev/null || true
|
||||
|
||||
case "$OPDS_SERVER" in
|
||||
dir2opds)
|
||||
docker run -d \
|
||||
--name dir2opds \
|
||||
--restart unless-stopped \
|
||||
--network opds-net \
|
||||
--mount "type=bind,source=${BOOKS_PATH},target=/books,readonly,bind-propagation=shared" \
|
||||
"$OPDS_IMAGE" \
|
||||
/dir2opds -dir /books -hide-dot-files -calibre
|
||||
;;
|
||||
stump)
|
||||
mkdir -p "${OPDS_DIR}/stump-config"
|
||||
chown -R "$HUB_USER":"$HUB_USER" "${OPDS_DIR}"
|
||||
docker run -d \
|
||||
--name stump \
|
||||
--restart unless-stopped \
|
||||
--network opds-net \
|
||||
-v "${BOOKS_PATH}:/books:ro" \
|
||||
-v "${OPDS_DIR}/stump-config:/config" \
|
||||
-e PUID=1000 \
|
||||
-e PGID=1000 \
|
||||
"$OPDS_IMAGE"
|
||||
;;
|
||||
esac
|
||||
|
||||
info "$OPDS_SERVER started on internal network opds-net."
|
||||
|
||||
header "Writing Caddyfile"
|
||||
CADDY_DIR="${OPDS_DIR}/caddy"
|
||||
mkdir -p "${CADDY_DIR}/data" "${CADDY_DIR}/config"
|
||||
|
||||
if [ "$AUTO_HTTPS" = false ]; then
|
||||
TLS_BLOCK=" tls ${CERT_PATH} ${KEY_PATH}"
|
||||
else
|
||||
TLS_BLOCK=""
|
||||
fi
|
||||
|
||||
if [[ "${USE_AUTH,,}" == "y" ]]; then
|
||||
docker run --rm caddy:alpine caddy hash-password --plaintext "$OPDS_PASS" > /tmp/opds_hash.txt 2>/dev/null
|
||||
HASHED_PASS=$(cat /tmp/opds_hash.txt)
|
||||
rm -f /tmp/opds_hash.txt
|
||||
AUTH_BLOCK=" basicauth {
|
||||
${OPDS_USER} ${HASHED_PASS}
|
||||
}"
|
||||
else
|
||||
AUTH_BLOCK=""
|
||||
fi
|
||||
|
||||
if [ "$AUTO_HTTPS" = false ]; then
|
||||
cat > "${CADDY_DIR}/Caddyfile" << EOF
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:80 {
|
||||
redir https://{host}{uri} permanent
|
||||
}
|
||||
|
||||
${OPDS_DOMAIN} {
|
||||
encode gzip
|
||||
${TLS_BLOCK}
|
||||
${AUTH_BLOCK}
|
||||
reverse_proxy http://${OPDS_SERVER}:${OPDS_INTERNAL_PORT} {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
else
|
||||
cat > "${CADDY_DIR}/Caddyfile" << EOF
|
||||
${OPDS_DOMAIN} {
|
||||
encode gzip
|
||||
${AUTH_BLOCK}
|
||||
reverse_proxy http://${OPDS_SERVER}:${OPDS_INTERNAL_PORT} {
|
||||
header_up Host {host}
|
||||
header_up X-Real-IP {remote}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
|
||||
info "Caddyfile written to ${CADDY_DIR}/Caddyfile"
|
||||
|
||||
header "Firewall Check"
|
||||
warn "Caddy requires ports 80 and 443 to be open on this server."
|
||||
warn "If using a cloud firewall (e.g. Linode), ensure inbound TCP rules allow:"
|
||||
warn " Port 80 — required for Let's Encrypt HTTP challenge and HTTP→HTTPS redirect"
|
||||
warn " Port 443 — required for HTTPS"
|
||||
echo ""
|
||||
read -rp "Press ENTER to continue once ports are open..."
|
||||
|
||||
header "Starting Caddy"
|
||||
docker rm -f caddy-opds 2>/dev/null || true
|
||||
|
||||
if [ "$AUTO_HTTPS" = true ]; then
|
||||
docker run -d \
|
||||
--name caddy-opds \
|
||||
--restart unless-stopped \
|
||||
--network opds-net \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-v "${CADDY_DIR}/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||
-v "${CADDY_DIR}/data:/data" \
|
||||
-v "${CADDY_DIR}/config:/config" \
|
||||
caddy:alpine
|
||||
else
|
||||
CERT_DIR=$(dirname "$CERT_PATH")
|
||||
docker run -d \
|
||||
--name caddy-opds \
|
||||
--restart unless-stopped \
|
||||
--network opds-net \
|
||||
-p 80:80 \
|
||||
-p 443:443 \
|
||||
-v "${CADDY_DIR}/Caddyfile:/etc/caddy/Caddyfile:ro" \
|
||||
-v "${CADDY_DIR}/data:/data" \
|
||||
-v "${CADDY_DIR}/config:/config" \
|
||||
-v "${CERT_DIR}:/certs:ro" \
|
||||
caddy:alpine
|
||||
fi
|
||||
|
||||
sleep 3
|
||||
docker logs caddy-opds --tail 5 2>/dev/null || true
|
||||
|
||||
header "Setting Up Mount Watchdog"
|
||||
|
||||
WATCHDOG_SCRIPT="/usr/local/bin/opds-watchdog.sh"
|
||||
cat > "$WATCHDOG_SCRIPT" << EOF
|
||||
#!/usr/bin/env bash
|
||||
SERVER_NAME="${OPDS_SERVER}"
|
||||
MAX_WAIT=300
|
||||
ELAPSED=0
|
||||
while [ \$ELAPSED -lt \$MAX_WAIT ]; do
|
||||
COUNT=\$(docker exec "\$SERVER_NAME" ls /books 2>/dev/null | wc -l)
|
||||
if [ "\$COUNT" -gt 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
docker restart "\$SERVER_NAME" 2>/dev/null || true
|
||||
sleep 15
|
||||
ELAPSED=\$((ELAPSED + 15))
|
||||
done
|
||||
EOF
|
||||
chmod +x "$WATCHDOG_SCRIPT"
|
||||
|
||||
MOUNT_SCRIPT="/home/${HUB_USER}/mount-$(basename ${MOUNT_POINT:-grace}).sh"
|
||||
cat > "$MOUNT_SCRIPT" << EOF
|
||||
#!/usr/bin/env bash
|
||||
sleep 90
|
||||
/usr/bin/rclone mount ${OPDS_SERVER}-remote: ${MOUNT_POINT:-/home/${HUB_USER}/mnt/grace} --config /home/${HUB_USER}/.config/rclone/rclone.conf --vfs-cache-mode writes --allow-other --allow-non-empty --daemon
|
||||
EOF
|
||||
chmod +x "$MOUNT_SCRIPT"
|
||||
chown "$HUB_USER":"$HUB_USER" "$MOUNT_SCRIPT"
|
||||
|
||||
CRON_LINE="@reboot $WATCHDOG_SCRIPT"
|
||||
EXISTING_ROOT_CRON=$(crontab -l 2>/dev/null || true)
|
||||
if ! echo "$EXISTING_ROOT_CRON" | grep -qF "opds-watchdog"; then
|
||||
{ echo "$EXISTING_ROOT_CRON"; echo "$CRON_LINE"; } | crontab -
|
||||
info "Watchdog crontab entry added — restarts $OPDS_SERVER until books are visible."
|
||||
else
|
||||
crontab -l 2>/dev/null | grep -v "opds-watchdog" | { cat; echo "$CRON_LINE"; } | crontab -
|
||||
info "Watchdog crontab entry updated."
|
||||
fi
|
||||
|
||||
header "OPDS Setup Complete"
|
||||
echo ""
|
||||
echo -e " OPDS URL: ${GREEN}https://${OPDS_DOMAIN}${NC}"
|
||||
if [[ "${USE_AUTH,,}" == "y" ]]; then
|
||||
echo -e " Username: ${GREEN}${OPDS_USER}${NC}"
|
||||
echo -e " Password: ${GREEN}(as entered)${NC}"
|
||||
else
|
||||
echo -e " Auth: ${YELLOW}None — publicly accessible${NC}"
|
||||
fi
|
||||
if [ "$AUTO_HTTPS" = true ]; then
|
||||
echo -e " SSL: ${GREEN}Automatic (Let's Encrypt)${NC}"
|
||||
warn "DNS must point ${OPDS_DOMAIN} to this server's IP for HTTPS to work."
|
||||
else
|
||||
echo -e " SSL: ${GREEN}Manual certificates${NC}"
|
||||
fi
|
||||
echo ""
|
||||
143
syncthing.sh
143
syncthing.sh
@@ -84,6 +84,145 @@ for device_id, info in devices.items():
|
||||
fi
|
||||
}
|
||||
|
||||
show_ignored_folders() {
|
||||
local config
|
||||
config=$(st_get /rest/config)
|
||||
local ignored
|
||||
ignored=$(echo "$config" | python3 -c '
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
folders = d.get("ignoredFolders", [])
|
||||
for f in folders:
|
||||
print(f.get("id","?") + " — " + f.get("label","(no label)"))
|
||||
' 2>/dev/null || true)
|
||||
if [ -z "$ignored" ]; then
|
||||
info "No ignored folders."
|
||||
return
|
||||
fi
|
||||
echo ""
|
||||
warn "Ignored folders (click Ignore in WUI causes this):"
|
||||
echo "$ignored" | sed 's/^/ /'
|
||||
echo ""
|
||||
read -rp "Un-ignore a folder? [y/N]: " UNIGNORE
|
||||
UNIGNORE="${UNIGNORE:-n}"
|
||||
if [[ "${UNIGNORE,,}" != "y" ]]; then return; fi
|
||||
|
||||
local folder_ids
|
||||
folder_ids=$(echo "$config" | python3 -c '
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
for f in d.get("ignoredFolders",[]):
|
||||
print(f.get("id",""))
|
||||
' 2>/dev/null || true)
|
||||
|
||||
echo "Ignored folder IDs:"
|
||||
local i=1
|
||||
while IFS= read -r fid; do
|
||||
echo " $i) $fid"
|
||||
i=$((i+1))
|
||||
done <<< "$folder_ids"
|
||||
read -rp "Choose folder number to un-ignore: " CHOICE
|
||||
local TARGET_ID
|
||||
TARGET_ID=$(echo "$folder_ids" | sed -n "${CHOICE}p")
|
||||
[ -n "$TARGET_ID" ] || { warn "Invalid choice."; return; }
|
||||
|
||||
local new_config
|
||||
new_config=$(echo "$config" | python3 -c "
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
d['ignoredFolders'] = [f for f in d.get('ignoredFolders',[]) if f.get('id') != sys.argv[1]]
|
||||
print(json.dumps(d))
|
||||
" "$TARGET_ID")
|
||||
st_put /rest/config "$new_config"
|
||||
info "Folder $TARGET_ID removed from ignored list. It should now appear as pending."
|
||||
}
|
||||
|
||||
show_pending_folders() {
|
||||
header "Pending Folders"
|
||||
local pending
|
||||
pending=$(st_get /rest/cluster/pending/folders)
|
||||
local count
|
||||
count=$(echo "$pending" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d))')
|
||||
if [ "$count" -eq 0 ]; then
|
||||
warn "No pending folders."
|
||||
echo ""
|
||||
read -rp "Check ignored folders? [y/N]: " CHECK_IGNORED
|
||||
if [[ "${CHECK_IGNORED,,}" == "y" ]]; then
|
||||
show_ignored_folders
|
||||
fi
|
||||
return
|
||||
fi
|
||||
echo "$pending" | python3 -c '
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
for fid,info in d.items():
|
||||
label = info.get("label", "") or info.get("offeredBy", {})
|
||||
label = list(info.get("offeredBy",{}).values())[0].get("receiveEncrypted","") if info.get("offeredBy") else ""
|
||||
offered_by = list(info.get("offeredBy",{}).keys())
|
||||
folder_label = ""
|
||||
for dev_info in info.get("offeredBy",{}).values():
|
||||
if dev_info.get("folderLabel"):
|
||||
folder_label = dev_info["folderLabel"]
|
||||
break
|
||||
if folder_label:
|
||||
print(f" Folder Name: {folder_label}")
|
||||
print(f" Folder ID: {fid}")
|
||||
print(f" Offered by: {offered_by}")
|
||||
print("")
|
||||
'
|
||||
read -rp "Accept a pending folder? [y/N/R (retry/check ignored)]: " ACCEPT
|
||||
ACCEPT="${ACCEPT:-n}"
|
||||
if [[ "${ACCEPT,,}" == "r" ]]; then
|
||||
show_ignored_folders
|
||||
return
|
||||
fi
|
||||
if [[ "${ACCEPT,,}" != "y" ]]; then return; fi
|
||||
|
||||
local folder_ids folder_labels
|
||||
folder_ids=$(echo "$pending" | python3 -c 'import sys,json; [print(k) for k in json.load(sys.stdin)]')
|
||||
folder_labels=$(echo "$pending" | python3 -c '
|
||||
import sys,json
|
||||
d=json.load(sys.stdin)
|
||||
for fid,info in d.items():
|
||||
label=""
|
||||
for dev_info in info.get("offeredBy",{}).values():
|
||||
if dev_info.get("folderLabel"):
|
||||
label=dev_info["folderLabel"]
|
||||
break
|
||||
print(label if label else fid)
|
||||
')
|
||||
local FOLDER_ID
|
||||
if [ "$(echo "$folder_ids" | wc -l)" -eq 1 ]; then
|
||||
FOLDER_ID="$folder_ids"
|
||||
else
|
||||
echo "Available pending folders:"
|
||||
local i=1
|
||||
while IFS= read -r fid && IFS= read -r flabel <&3; do
|
||||
echo " $i) $flabel ($fid)"
|
||||
i=$((i+1))
|
||||
done <<< "$folder_ids" 3<<< "$folder_labels"
|
||||
read -rp "Choose folder number: " FOLDER_NUM
|
||||
FOLDER_ID=$(echo "$folder_ids" | sed -n "${FOLDER_NUM}p")
|
||||
fi
|
||||
[ -n "$FOLDER_ID" ] || { warn "No folder selected."; return; }
|
||||
|
||||
echo ""
|
||||
info "Available directories in /var/syncthing/data/:"
|
||||
ls /var/syncthing/data/ 2>/dev/null | sed 's/^/ /' || true
|
||||
echo ""
|
||||
read -rp "Folder path on this device: " FOLDER_PATH
|
||||
[ -n "$FOLDER_PATH" ] || { warn "Path cannot be empty."; return; }
|
||||
|
||||
read -rp "Folder label [$FOLDER_ID]: " FOLDER_LABEL
|
||||
FOLDER_LABEL="${FOLDER_LABEL:-$FOLDER_ID}"
|
||||
|
||||
st_post /rest/config/folders "$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({'id': '$FOLDER_ID', 'label': '$FOLDER_LABEL', 'path': '$FOLDER_PATH'}))
|
||||
")"
|
||||
info "Folder '$FOLDER_LABEL' added at $FOLDER_PATH."
|
||||
}
|
||||
|
||||
add_device_by_pending() {
|
||||
local pending="$1"
|
||||
local ids
|
||||
@@ -436,7 +575,7 @@ get_apikey
|
||||
while true; do
|
||||
header "Syncthing Manager"
|
||||
echo " 0) Show This Device's ID"
|
||||
echo " 1) Pending Devices"
|
||||
echo " 1) Pending Devices & Folders"
|
||||
echo " 2) List Devices"
|
||||
echo " 3) Add Device"
|
||||
echo " 4) Remove Device"
|
||||
@@ -451,7 +590,7 @@ while true; do
|
||||
echo ""
|
||||
case "$OPT" in
|
||||
0) show_own_device_id ;;
|
||||
1) show_pending_devices ;;
|
||||
1) show_pending_devices; show_pending_folders ;;
|
||||
2) list_devices ;;
|
||||
3) add_device ;;
|
||||
4) remove_device ;;
|
||||
|
||||
Reference in New Issue
Block a user