diff --git a/deploy/node/upgrade-node.sh b/deploy/node/upgrade-node.sh index 90c383d..61b4a4f 100644 --- a/deploy/node/upgrade-node.sh +++ b/deploy/node/upgrade-node.sh @@ -1,5 +1,7 @@ #!/bin/bash # Upgrade media-node binary (download latest from Master + restart container). +# Op Synology: herstelt ook /opt/media-node-bin + MEDIA_NODE_BIN_DIR als die ontbreken +# (anders faalt Admin→Upgrade met docker.sock / media-node.new permission denied). set -euo pipefail MASTER_URL="${MASTER_URL:-__MASTER_PUBLIC_URL__}" @@ -11,6 +13,9 @@ usage() { Gebruik: curl -fsSL ${MASTER_URL}/install/upgrade-node.sh | bash -s -- --appdata=/mnt/user/appdata/media-node +Synology voorbeeld: + curl -fsSL ${MASTER_URL}/install/upgrade-node.sh | bash -s -- --appdata=/volume1/docker/media-node + Opties: --appdata= Appdata-map met bin/media-node (Unraid: /mnt/user/appdata/media-node, Synology: /volume1/docker/media-node) --container= Docker-containernaam (default: media-node) @@ -29,7 +34,7 @@ for arg in "$@"; do done MASTER_URL="$(echo -n "$MASTER_URL" | sed 's:/*$::')" -APPDATA="$(echo -n "$APPDATA" | tr -d '\r\n')" +APPDATA="$(echo -n "$APPDATA" | tr -d '\r\n' | sed 's:/*$::')" docker_cmd() { if docker info >/dev/null 2>&1; then @@ -69,11 +74,101 @@ echo echo "Downloaden ($BIN_NAME)..." curl -fsSL "$MASTER_URL/install/$BIN_NAME" -o "$BIN_PATH.new" chmod +x "$BIN_PATH.new" -mv "$BIN_PATH.new" "$BIN_PATH" +mv -f "$BIN_PATH.new" "$BIN_PATH" + +# Herstel Admin→Upgrade op Synology: directory-mount + env (file-mount alleen is niet genoeg). +ensure_bin_mount() { + if ! docker_cmd ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then + return 0 + fi + + local has_mount has_env + has_mount="$(docker_cmd inspect -f '{{range .Mounts}}{{if eq .Destination "/opt/media-node-bin"}}1{{end}}{{end}}' "$CONTAINER" 2>/dev/null || true)" + has_env="$(docker_cmd inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER" 2>/dev/null | grep -c '^MEDIA_NODE_BIN_DIR=' || true)" + + if [ "$has_mount" = "1" ] && [ "${has_env:-0}" -gt 0 ]; then + return 0 + fi + + echo + echo "Container mist /opt/media-node-bin en/of MEDIA_NODE_BIN_DIR — opnieuw aanmaken zodat Admin→Upgrade werkt..." + + local user port image + user="$(docker_cmd inspect -f '{{.Config.User}}' "$CONTAINER")" + [ -n "$user" ] || user="1026:100" + port="$(docker_cmd inspect -f '{{(index (index .HostConfig.PortBindings "8080/tcp") 0).HostPort}}' "$CONTAINER" 2>/dev/null || true)" + [ -n "$port" ] || port="8080" + image="$(docker_cmd inspect -f '{{.Config.Image}}' "$CONTAINER")" + [ -n "$image" ] || image="alpine:3.20" + + # Env behouden, MEDIA_NODE_BIN_DIR forceren, enrollment-token weglaten (credentials bestaan al). + local -a env_args=() + local line + while IFS= read -r line; do + [ -z "$line" ] && continue + case "$line" in + MEDIA_NODE_ENROLLMENT_TOKEN=*) continue ;; + MEDIA_NODE_BIN_DIR=*) continue ;; + MEDIA_NODE_CONTAINER_NAME=*) continue ;; + esac + env_args+=(-e "$line") + done < <(docker_cmd inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$CONTAINER") + env_args+=(-e "MEDIA_NODE_BIN_DIR=/opt/media-node-bin") + env_args+=(-e "MEDIA_NODE_CONTAINER_NAME=$CONTAINER") + + # Bestaande mounts behouden, behalve bin-paden die we zelf opnieuw zetten. + local -a vol_args=() + local src dst mode + while IFS=$'\t' read -r src dst mode; do + [ -z "$dst" ] && continue + case "$dst" in + /usr/local/bin/media-node|/opt/media-node-bin) continue ;; + esac + if [ "$mode" = "ro" ]; then + vol_args+=(-v "$src:$dst:ro") + else + vol_args+=(-v "$src:$dst") + fi + done < <(docker_cmd inspect -f '{{range .Mounts}}{{.Source}}{{"\t"}}{{.Destination}}{{"\t"}}{{if .RW}}rw{{else}}ro{{end}}{{"\n"}}{{end}}' "$CONTAINER") + + vol_args+=(-v "$BIN_PATH:/usr/local/bin/media-node:rw") + vol_args+=(-v "$BIN_DIR:/opt/media-node-bin:rw") + + local -a group_args=() + if [ -S /var/run/docker.sock ]; then + local docker_gid + docker_gid="$(stat -c '%g' /var/run/docker.sock 2>/dev/null || true)" + if [ -n "$docker_gid" ] && [ "$docker_gid" != "0" ]; then + group_args+=(--group-add "$docker_gid") + fi + fi + + docker_cmd stop "$CONTAINER" >/dev/null + docker_cmd rm "$CONTAINER" >/dev/null + + docker_cmd run -d \ + --name "$CONTAINER" \ + --restart unless-stopped \ + -p "${port}:8080" \ + --user "$user" \ + "${group_args[@]}" \ + "${env_args[@]}" \ + "${vol_args[@]}" \ + --entrypoint /usr/local/bin/media-node \ + "$image" \ + run + + echo "Container opnieuw aangemaakt met /opt/media-node-bin (Admin→Upgrade zou nu moeten werken)." + return 0 +} if docker_cmd ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then - echo "Container herstarten..." - docker_cmd restart "$CONTAINER" >/dev/null + ensure_bin_mount || true + if docker_cmd ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then + # Als ensure_bin_mount al opnieuw startte, restart nog eens zodat verse binary zeker geladen is. + echo "Container herstarten..." + docker_cmd restart "$CONTAINER" >/dev/null + fi echo "Klaar. Controleer versie in Admin → Nodes (verwacht ${MEDIA_NODE_VERSION:-1.3.12}+)." else echo "Waarschuwing: container '$CONTAINER' niet gevonden — binary is wel bijgewerkt." diff --git a/node/media-node/internal/control/upgrade.go b/node/media-node/internal/control/upgrade.go index 24d7532..1bc98ab 100644 --- a/node/media-node/internal/control/upgrade.go +++ b/node/media-node/internal/control/upgrade.go @@ -85,7 +85,9 @@ func (c *Client) runUpgrade(downloadURL string) error { if _, err := os.Stat("/var/run/docker.sock"); err != nil { return fmt.Errorf( - "binary niet te vervangen (%s) en geen docker.sock — eenmalig via SSH: curl -fsSL \"$MASTER/install/upgrade-node.sh\" | bash -s -- --appdata=", + "binary niet te vervangen (%s). Op Synology: eenmalig via SSH upgraden én bin-mount herstellen:\n"+ + " curl -fsSL \"$MASTER/install/upgrade-node.sh\" | bash -s -- --appdata=\n"+ + "(script zet /opt/media-node-bin + MEDIA_NODE_BIN_DIR zodat Admin→Upgrade daarna werkt)", strings.Join(errs, "; "), ) } @@ -97,7 +99,11 @@ func (c *Client) runUpgrade(downloadURL string) error { binHostDir, dataHostDir, err := dockerContainerBindDirs(containerName) if err != nil { - return fmt.Errorf("%w — eerdere pogingen: %s", err, strings.Join(errs, "; ")) + return fmt.Errorf( + "%w — eerdere pogingen: %s (Synology: docker.sock is vaak root-only; gebruik upgrade-node.sh via SSH)", + err, + strings.Join(errs, "; "), + ) } stagingHost := filepath.Join(dataHostDir, "upgrade", "media-node") @@ -113,17 +119,32 @@ func (c *Client) runUpgrade(downloadURL string) error { } func upgradeBinaryTargets() []string { + seen := map[string]bool{} var out []string - if binDir := strings.TrimSpace(os.Getenv("MEDIA_NODE_BIN_DIR")); binDir != "" { - out = append(out, filepath.Join(binDir, "media-node")) + add := func(p string) { + p = filepath.Clean(p) + if p == "" || p == "." || seen[p] { + return + } + seen[p] = true + out = append(out, p) } - // File bind-mount of the running entrypoint — often ETXTBSY; try last. - out = append(out, "/usr/local/bin/media-node") + + if binDir := strings.TrimSpace(os.Getenv("MEDIA_NODE_BIN_DIR")); binDir != "" { + add(filepath.Join(binDir, "media-node")) + } + // Install-scripts mounten $APPDATA/bin hier — ook proberen als env op oude containers ontbreekt. + if st, err := os.Stat("/opt/media-node-bin"); err == nil && st.IsDir() { + add("/opt/media-node-bin/media-node") + } + // File bind-mount van de entrypoint — .new ernaast faalt vaak (map is root-owned). + add("/usr/local/bin/media-node") return out } // installBinaryReplace writes dest.new then replaces dest (unlink+rename). // Unlink of a running Linux binary is allowed; open(O_WRONLY) on it is not (ETXTBSY). +// .new must live in the same directory as dest (directory bind-mount like /opt/media-node-bin). func installBinaryReplace(src, dest string) error { if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { return fmt.Errorf("bin-map: %w", err) @@ -132,29 +153,9 @@ func installBinaryReplace(src, dest string) error { newPath := dest + ".new" _ = os.Remove(newPath) - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - - out, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) - if err != nil { + if err := copyFile(src, newPath, 0o755); err != nil { return fmt.Errorf("schrijven %s: %w", newPath, err) } - if _, err := io.Copy(out, in); err != nil { - out.Close() - _ = os.Remove(newPath) - return fmt.Errorf("kopiëren: %w", err) - } - if err := out.Close(); err != nil { - _ = os.Remove(newPath) - return err - } - if err := os.Chmod(newPath, 0o755); err != nil { - _ = os.Remove(newPath) - return err - } // Drop old name (ok while process still runs), then put new binary in place. if err := os.Remove(dest); err != nil && !os.IsNotExist(err) { @@ -172,6 +173,29 @@ func installBinaryReplace(src, dest string) error { return nil } +func copyFile(src, dest string, mode os.FileMode) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) + if err != nil { + return err + } + if _, err := io.Copy(out, in); err != nil { + out.Close() + _ = os.Remove(dest) + return err + } + if err := out.Close(); err != nil { + _ = os.Remove(dest) + return err + } + return os.Chmod(dest, mode) +} + func downloadFile(url, dest string) error { log.Printf("UPGRADE: download %s", url) tmp := dest + ".download"