最後活躍 4 days ago

Hardened install scripts with an interactive menu — Chrome, Firefox, Thunderbird, 1Password, Espanso, LibreOffice, Obsidian, draw.io Desktop, VS Code, JetBrains Toolbox, Bruno, IPATool, Synology NAS auto-mount, IBus Pinyin, Nerd Fonts, qBittorrent LocalSend Telegram Discord.

README.md 原始檔案

Ubuntu / Debian Setup Manager

A collection of hardened install scripts for a fresh Ubuntu / Debian desktop, driven by an interactive menu.sh. Each script auto-detects the distro, uses signed-by APT keyrings where applicable (no apt-key), and is idempotent (safe to re-run).

Quick start — run the menu

One-liner — fetches menu.sh and runs it. You'll see a numeric picker and can select one option, several, or all:

bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)"

How to use the picker:

  • Enter one or more numbers separated by spaces, e.g. 1 6 11
  • 0 runs every option in order
  • -1 exits

The menu caches sudo credentials up-front so multi-task runs don't keep re-prompting, and falls back to per-user execution for the three scripts that must not run as root (JetBrains Toolbox, IBus Pinyin, Nerd Fonts).

Available scripts

# Script Runs as What it does
1 install-chrome.sh sudo Installs Google Chrome Stable from Google's official APT repo using a signed-by keyring.
2 install-firefox.sh sudo Removes the Firefox Snap and installs Firefox from Mozilla's official APT repo, with APT pinning so it stays on the Mozilla build. Verifies the Mozilla signing-key fingerprint.
3 install-thunderbird.sh sudo Ubuntu: removes the Snap, adds the Mozilla Team PPA, and pins it. Debian: installs from the standard repos.
4 install-localsend.sh sudo Installs the latest official LocalSend .deb from localsend/localsend. SHA-256 verified against the GitHub release asset digest when available.
5 install-telegram.sh sudo Installs the latest official Telegram Desktop Linux build from Telegram's latest download endpoint. Adds launcher and tg: URL handler.
6 install-discord.sh sudo Installs the latest official Discord Linux .deb from Discord's latest download endpoint.
7 install-1password.sh sudo Configures the 1Password APT repo with debsig signature policy. Arch-aware (amd64 / arm64).
8 install-espanso.sh sudo Installs Espanso. Auto-detects Wayland vs X11 from $XDG_SESSION_TYPE and registers the systemd-user service as the invoking desktop user (not root).
9 install-libreoffice.sh sudo Detects the latest stable release on documentfoundation.org, downloads the matching .deb tarball, verifies the published MD5, and installs. Purges the distro's libreoffice* first to avoid library conflicts (opt-out with --keep-distro-libreoffice).
10 install-obsidian.sh sudo Installs the latest official Obsidian amd64 .deb from obsidianmd/obsidian-releases. SHA-256 verified against the GitHub release asset digest when available. Honors $GITHUB_TOKEN to avoid API rate limits.
11 install-drawio.sh sudo Installs the latest official draw.io Desktop .deb from jgraph/drawio-desktop. SHA-256 verified against the GitHub release asset digest when available. Honors $GITHUB_TOKEN to avoid API rate limits.
12 install-vscode.sh sudo Microsoft's official code APT repo, signed-by keyring. --insiders flag installs code-insiders instead.
13 install-jetbrains-toolbox.sh user Per-user install into ~/.local/share/JetBrains/Toolbox. SHA-256 verified against JetBrains' release feed. x86_64 + aarch64. Drops a .desktop launcher.
14 install-bruno.sh sudo Bruno API client from the official APT repo. Keyserver fetch is wrapped in a 5-attempt retry/backoff because keyserver.ubuntu.com is occasionally flaky.
15 install-ipatool.sh sudo Installs the latest release of majd/ipatool from GitHub. SHA-256 verified against the release checksums.txt. Honors $GITHUB_TOKEN to avoid API rate limits.
16 install-qbittorrent.sh sudo Installs the latest official qBittorrent x86_64 AppImage from qbittorrent/qBittorrent. SHA-256 verified against the GitHub release asset digest when available. Installs launcher, icons, and torrent/magnet handlers.
17 install-network_drive.sh sudo Discovers SMB shares on a Synology NAS and adds them to /etc/fstab under /mnt/Synology with x-systemd.automount. fstab block is managed via begin/end markers so re-runs replace rather than duplicate. Credentials file is 0600. Best-effort GNOME Dock pin.
18 install-ibus-pinyin.sh user Installs ibus-libpinyin and Simplified Chinese language packs, restarts the IBus daemon, and idempotently adds ('ibus', 'libpinyin') to GNOME's input sources via gsettings.
19 install-font.sh user Installs the latest Ubuntu Sans Nerd Font and JetBrains Mono Nerd Font to ~/.local/share/fonts, then refreshes the font cache. No sudo needed.
20 timedatectl-fix.sh sudo Sets the hardware clock to UTC to avoid time drift when dual-booting Linux and Windows.
21 install-screenshot-cleanup-cron.sh user Installs an idempotent per-user cron job that clears $HOME/Pictures/Screenshots every 5 minutes while preserving the directory itself.

"Runs as user" entries must be invoked as your normal desktop user, not via sudo. The other entries elevate via sudo internally and the menu primes sudo -v up-front, so you'll only be prompted once.

Running a single script directly

If you'd rather skip the menu, each script can be run on its own. Use the right invocation pattern for that script's privilege mode:

# sudo scripts (1-12, 14-17, 20) — pipe through sudo bash
curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-firefox.sh | sudo bash

# user scripts (13, 18, 19, 21) — DO NOT use sudo; they install per-user
bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-font.sh)"

Most installers accept --help and --dry-run (the latter prints what would happen without executing). For example:

curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/install-libreoffice.sh \
  | bash -s -- --help

What "hardened" means here

The hardened installers generally share this robustness baseline:

  • set -euo pipefail + IFS hygiene + ERR trap reporting the failing line number
  • /etc/os-release distro auto-detection (no hardcoded codenames)
  • dpkg --print-architecture / uname -m for architecture (amd64 / arm64 / armhf / x86_64 / aarch64 as applicable to each upstream)
  • Idempotent — re-running a script does not duplicate APT sources, fstab entries, gsettings entries, or .desktop files
  • signed-by keyrings in /etc/apt/keyrings (no deprecated apt-key add)
  • Checksum verification where the upstream publishes one (JetBrains SHA-256, LibreOffice MD5, Obsidian SHA-256, ipatool SHA-256, draw.io SHA-256, qBittorrent SHA-256, LocalSend SHA-256)
  • Per-user installers refuse to run as root; system installers refuse to run as non-root

Supported distros

  • Ubuntu (any modern release; some scripts target Ubuntu 25.10 specifically but work elsewhere)
  • Debian (most scripts; install-thunderbird.sh takes a different code path since Debian has no PPAs)

Other distros are rejected up-front rather than failing later in unpredictable ways.

install-1password.sh 原始檔案
1#!/usr/bin/env bash
2# install-1password.sh — Install 1Password desktop from the official 1Password APT repo.
3# Hardened: arch-aware, signed-by keyring, debsig verification dir, idempotent, --dry-run.
4
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10
11usage() {
12 cat <<EOF
13Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
14
15Configures the official 1Password APT repository (with debsig policy and
16keyring) and installs the 1Password desktop application. Idempotent: safe to
17re-run.
18
19Options:
20 --dry-run Print actions without executing.
21 --help, -h Show this help.
22EOF
23}
24
25log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
26warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
27die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
28run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
29trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
30
31while (( $# )); do
32 case "$1" in
33 --dry-run) DRY_RUN=1 ;;
34 -h|--help) usage; exit 0 ;;
35 *) die "Unknown argument: $1 (try --help)" ;;
36 esac
37 shift
38done
39
40(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
41
42[[ -r /etc/os-release ]] || die "/etc/os-release not found."
43# shellcheck disable=SC1091
44. /etc/os-release
45case "${ID:-}:${ID_LIKE:-}" in
46 *ubuntu*|*debian*) : ;;
47 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
48esac
49
50# 1Password publishes builds for amd64 and arm64.
51ARCH="$(dpkg --print-architecture)"
52case "$ARCH" in
53 amd64|arm64) : ;;
54 *) die "1Password is only available for amd64/arm64 (detected: $ARCH)." ;;
55esac
56log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
57
58export DEBIAN_FRONTEND=noninteractive
59
60KEYRING=/etc/apt/keyrings/1password-archive-keyring.gpg
61SOURCES=/etc/apt/sources.list.d/1password.list
62DEBSIG_POLICY_DIR=/etc/debsig/policies/AC2D62742012EA22
63DEBSIG_KEYRING_DIR=/usr/share/debsig/keyrings/AC2D62742012EA22
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl gpg ca-certificates"
68
69log "Configuring 1Password APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] https://downloads.1password.com/linux/debian/${ARCH} stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing debsig policy (verifies package signatures on install)..."
82run "install -d -m 0755 '$DEBSIG_POLICY_DIR' '$DEBSIG_KEYRING_DIR'"
83if [[ ! -s "${DEBSIG_POLICY_DIR}/1password.pol" ]]; then
84 run "curl -fsSL https://downloads.1password.com/linux/debian/debsig/1password.pol -o '${DEBSIG_POLICY_DIR}/1password.pol'"
85fi
86if [[ ! -s "${DEBSIG_KEYRING_DIR}/debsig.gpg" ]]; then
87 run "curl -fsSL https://downloads.1password.com/linux/keys/1password.asc | gpg --dearmor -o '${DEBSIG_KEYRING_DIR}/debsig.gpg'"
88fi
89
90log "Installing 1Password..."
91run "apt-get update -qq"
92run "apt-get install -y 1password"
93
94if (( ! DRY_RUN )) && command -v 1password >/dev/null 2>&1; then
95 log "1Password binary available at: $(command -v 1password)"
96fi
97log "Done."
98
install-bruno.sh 原始檔案
1#!/usr/bin/env bash
2# install-bruno.sh — Install Bruno API client from the official APT repository.
3# Hardened: arch-aware, signed-by keyring with retries (keyserver flake), idempotent,
4# clean desktop entry, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
15
16Configures the official Bruno APT repository (signed-by keyring fetched from
17keyserver.ubuntu.com with retries) and installs Bruno. Idempotent: safe to
18re-run.
19
20Options:
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); do
33 case "$1" in
34 --dry-run) DRY_RUN=1 ;;
35 -h|--help) usage; exit 0 ;;
36 *) die "Unknown argument: $1 (try --help)" ;;
37 esac
38 shift
39done
40
41(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
42
43[[ -r /etc/os-release ]] || die "/etc/os-release not found."
44# shellcheck disable=SC1091
45. /etc/os-release
46case "${ID:-}:${ID_LIKE:-}" in
47 *ubuntu*|*debian*) : ;;
48 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
49esac
50
51ARCH="$(dpkg --print-architecture)"
52[[ "$ARCH" == "amd64" ]] || die "Bruno APT repo only ships amd64 (detected: $ARCH)."
53log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
54
55export DEBIAN_FRONTEND=noninteractive
56
57KEYRING=/etc/apt/keyrings/bruno.gpg
58SOURCES=/etc/apt/sources.list.d/bruno.list
59KEY_ID="9FA6017ECABE0266"
60DESKTOP=/usr/share/applications/bruno.desktop
61
62log "Installing prerequisites..."
63run "apt-get update -qq"
64run "apt-get install -y curl gpg ca-certificates"
65
66log "Configuring Bruno APT repository..."
67run "install -d -m 0755 /etc/apt/keyrings"
68
69if [[ ! -s "$KEYRING" ]]; then
70 # keyserver.ubuntu.com is occasionally flaky — retry a few times.
71 ok=0
72 for attempt in 1 2 3 4 5; do
73 if (( DRY_RUN )); then
74 printf ' DRY-RUN: fetch key 0x%s (attempt %d)\n' "$KEY_ID" "$attempt"
75 ok=1
76 break
77 fi
78 if curl -fsSL --max-time 30 "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x${KEY_ID}" \
79 | gpg --dearmor -o "$KEYRING" 2>/dev/null && [[ -s "$KEYRING" ]]; then
80 ok=1
81 break
82 fi
83 warn "Key fetch failed (attempt $attempt/5); retrying in $((attempt*2))s..."
84 sleep "$((attempt*2))"
85 done
86 (( ok )) || die "Could not retrieve Bruno signing key after 5 attempts."
87 run "chmod 0644 '$KEYRING'"
88fi
89
90DESIRED_SRC="deb [arch=${ARCH} signed-by=${KEYRING}] http://debian.usebruno.com/ bruno stable"
91if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
92 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
93fi
94
95log "Installing Bruno..."
96run "apt-get update -qq"
97run "apt-get install -y bruno"
98
99log "Writing desktop entry..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$DESKTOP"
102else
103 cat >"$DESKTOP" <<'EOF'
104[Desktop Entry]
105Name=Bruno
106Comment=Open-source API Client
107Exec=bruno %U
108Terminal=false
109Type=Application
110Icon=bruno
111Categories=Development;Utility;
112StartupNotify=true
113EOF
114 chmod 0644 "$DESKTOP"
115fi
116
117log "Done. Bruno installed."
118
install-chrome.sh 原始檔案
1#!/usr/bin/env bash
2#
3# install-chrome.sh — Native Google Chrome Installer for Ubuntu/Debian
4# Downloads and installs the latest official stable release without Snap.
5#
6# Usage:
7# sudo ./install-chrome.sh
8
9set -euo pipefail
10IFS=$'\n\t'
11
12# Ensure the script is run as root
13if (( EUID != 0 )); then
14 echo "[ERROR] This script must be run as root (via sudo)." >&2
15 exit 1
16fi
17
18echo "[1/4] Installing necessary prerequisites..."
19apt-get update -y
20apt-get install -y curl gnupg2 ca-certificates
21
22echo "[2/4] Setting up Google's official repository keys..."
23# Create directory for Third-Party APT keys if it doesn't exist
24install -d -m 0755 /etc/apt/keyrings
25
26# Fetch the official signing key and save it securely
27curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | \
28 gpg --dearmor --yes -o /etc/apt/keyrings/google-chrome.gpg
29
30# Configure the APT repository to strictly use that signing key
31echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \
32 > /etc/apt/sources.list.d/google-chrome.list
33
34echo "[3/4] Fetching package lists and installing Google Chrome..."
35apt-get update -y
36apt-get install -y google-chrome-stable
37
38echo "[4/4] Verifying installation..."
39if command -v google-chrome &>/dev/null; then
40 echo "------------------------------------------------------"
41 echo "SUCCESS: Google Chrome has been installed successfully!"
42 echo "Version: $(google-chrome --version)"
43 echo "------------------------------------------------------"
44else
45 echo "[ERROR] Google Chrome installation could not be verified." >&2
46 exit 1
47fi
install-discord.sh 原始檔案
1#!/usr/bin/env bash
2# install-discord.sh — Install Discord from the latest official Linux .deb.
3# Hardened: official latest endpoint, idempotent, --dry-run.
4
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10DOWNLOAD_URL="https://discord.com/api/download?platform=linux&format=deb"
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
15
16Downloads Discord from Discord's official latest Linux .deb endpoint and
17installs it with apt-get.
18
19Options:
20 --dry-run Print actions without executing.
21 --help, -h Show this help.
22EOF
23}
24
25log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
26die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
27run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
28trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
29
30while (( $# )); do
31 case "$1" in
32 --dry-run) DRY_RUN=1 ;;
33 -h|--help) usage; exit 0 ;;
34 *) die "Unknown argument: $1 (try --help)" ;;
35 esac
36 shift
37done
38
39(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
40
41[[ -r /etc/os-release ]] || die "/etc/os-release not found."
42# shellcheck disable=SC1091
43. /etc/os-release
44case "${ID:-}:${ID_LIKE:-}" in
45 *ubuntu*|*debian*) : ;;
46 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
47esac
48
49ARCH="$(dpkg --print-architecture)"
50[[ "$ARCH" == "amd64" ]] || die "Discord's official Linux .deb targets amd64 only (detected: $ARCH)."
51log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
52
53export DEBIAN_FRONTEND=noninteractive
54log "Installing prerequisites..."
55run "apt-get update -qq"
56run "apt-get install -y curl ca-certificates"
57
58STAGE="$(mktemp -d -t discord.XXXXXX)"
59trap 'rm -rf "$STAGE"' EXIT
60DEB="$STAGE/discord.deb"
61
62log "Downloading latest Discord .deb..."
63run "curl -fL -o '$DEB' '$DOWNLOAD_URL'"
64
65if (( DRY_RUN )); then
66 printf ' DRY-RUN: apt-get install -y %s\n' "$DEB"
67 log "Done."
68 exit 0
69fi
70
71log "Installing Discord..."
72run "apt-get install -y '$DEB'"
73
74if command -v discord >/dev/null 2>&1; then
75 log "Installed: $(discord --version 2>/dev/null || dpkg-query -W -f='${Version}' discord 2>/dev/null || echo Discord)"
76else
77 log "Installed: $(dpkg-query -W -f='${Version}' discord 2>/dev/null || echo Discord)"
78fi
79log "Done."
80
install-drawio.sh 原始檔案
1#!/usr/bin/env bash
2# install-drawio.sh — Install draw.io Desktop from the latest official GitHub release.
3# Hardened: arch-aware, GitHub API token support, SHA-256 verification from the
4# release asset digest, idempotent, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="jgraph/drawio-desktop"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Resolves the latest release of jgraph/drawio-desktop, downloads the official
18Linux .deb package for your architecture, verifies its SHA-256 against the
19GitHub release asset digest when available, and installs it with apt-get.
20
21If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
22GitHub API request (avoids rate limits).
23
24Options:
25 --dry-run Print actions without executing.
26 --help, -h Show this help.
27EOF
28}
29
30log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
31warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
32die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
33run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
34trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
35
36while (( $# )); do
37 case "$1" in
38 --dry-run) DRY_RUN=1 ;;
39 -h|--help) usage; exit 0 ;;
40 *) die "Unknown argument: $1 (try --help)" ;;
41 esac
42 shift
43done
44
45(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
46
47[[ -r /etc/os-release ]] || die "/etc/os-release not found."
48# shellcheck disable=SC1091
49. /etc/os-release
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
53esac
54
55ARCH="$(dpkg --print-architecture)"
56case "$ARCH" in
57 amd64|arm64) : ;;
58 *) die "draw.io Desktop publishes Linux .deb assets for amd64 and arm64 only (detected: $ARCH)." ;;
59esac
60log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
61
62export DEBIAN_FRONTEND=noninteractive
63log "Installing prerequisites..."
64run "apt-get update -qq"
65run "apt-get install -y curl jq ca-certificates"
66
67GH_HDRS=(-H "Accept: application/vnd.github+json")
68[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
69
70log "Querying GitHub API for latest release of $REPO..."
71RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
72TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
73[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
74log "Latest release: $TAG"
75
76ASSET_REGEX="^drawio-${ARCH}-[^/]+\\.deb$"
77DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
78ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)"
79EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)"
80[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No $ARCH .deb asset found in latest draw.io Desktop release."
81
82STAGE="$(mktemp -d -t drawio.XXXXXX)"
83trap 'rm -rf "$STAGE"' EXIT
84DEB="$STAGE/$ASSET_NAME"
85
86log "Downloading $ASSET_NAME..."
87run "curl -fsSL -o '$DEB' '$DOWNLOAD_URL'"
88
89if (( DRY_RUN )); then
90 [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \
91 && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \
92 || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n'
93 printf ' DRY-RUN: apt-get install -y %s\n' "$DEB"
94 log "Done."
95 exit 0
96fi
97
98if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then
99 log "Verifying SHA-256..."
100 ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')"
101 [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
102 log "SHA-256 ok."
103else
104 warn "No SHA-256 asset digest in GitHub release metadata; skipping verification."
105fi
106
107log "Installing draw.io Desktop..."
108run "apt-get install -y '$DEB'"
109
110if (( ! DRY_RUN )) && command -v drawio >/dev/null 2>&1; then
111 log "Installed: $(drawio --version 2>/dev/null || echo "$TAG")"
112fi
113log "Done."
114
install-espanso.sh 原始檔案
1#!/usr/bin/env bash
2# install-espanso.sh — Install Espanso (text expander) with Wayland or X11 build.
3# Auto-detects session type; falls back to X11 if Wayland is not active.
4# Hardened: handles apt temp file permissions, package collisions, and ghost processes.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11FORCE_VARIANT="" # "wayland" | "x11"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--wayland|--x11] [--help]
16
17Installs Espanso from the latest GitHub release. Auto-detects whether to use
18the Wayland or X11 build based on \$XDG_SESSION_TYPE of the invoking user
19(SUDO_USER). The systemd-user service is registered for that user, not root.
20
21Options:
22 --wayland Force the Wayland build.
23 --x11 Force the X11 build.
24 --dry-run Print actions without executing.
25 --help, -h Show this help.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); do
36 case "$1" in
37 --dry-run) DRY_RUN=1 ;;
38 --wayland) FORCE_VARIANT=wayland ;;
39 --x11) FORCE_VARIANT=x11 ;;
40 -h|--help) usage; exit 0 ;;
41 *) die "Unknown argument: $1 (try --help)" ;;
42 esac
43 shift
44done
45
46(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
47
48[[ -r /etc/os-release ]] || die "/etc/os-release not found."
49# shellcheck disable=SC1091
50. /etc/os-release
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
54esac
55
56ARCH="$(dpkg --print-architecture)"
57[[ "$ARCH" == "amd64" ]] || die "Espanso .deb is published for amd64 only (detected: $ARCH)."
58
59ACTUAL_USER="${SUDO_USER:-${USER:-}}"
60[[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user; cannot register the service as root."
61USER_ID="$(id -u "$ACTUAL_USER")"
62
63# Detect session type from the invoking user's environment, fallback to env, fallback to wayland
64if [[ -n "$FORCE_VARIANT" ]]; then
65 VARIANT="$FORCE_VARIANT"
66else
67 SESSION_TYPE="$(sudo -u "$ACTUAL_USER" -i printenv XDG_SESSION_TYPE 2>/dev/null || true)"
68 [[ -z "$SESSION_TYPE" ]] && SESSION_TYPE="${XDG_SESSION_TYPE:-wayland}"
69 case "$SESSION_TYPE" in
70 wayland) VARIANT=wayland ;;
71 x11|tty) VARIANT=x11 ;;
72 *) warn "Unknown XDG_SESSION_TYPE='$SESSION_TYPE'; defaulting to wayland."; VARIANT=wayland ;;
73 esac
74fi
75log "Detected: ${PRETTY_NAME:-unknown}, user: $ACTUAL_USER, session: $VARIANT"
76
77export DEBIAN_FRONTEND=noninteractive
78TEMP_DEB="$(mktemp -t espanso.XXXXXX.deb)"
79# Ensure the _apt user can read the temporary file to prevent permission denied errors
80chmod 644 "$TEMP_DEB"
81trap 'rm -f "$TEMP_DEB"' EXIT
82
83DEB_NAME="espanso-debian-${VARIANT}-amd64.deb"
84URL="https://github.com/espanso/espanso/releases/latest/download/${DEB_NAME}"
85
86log "Installing prerequisites..."
87run "apt-get update -qq"
88run "apt-get install -y wget libcap2-bin psmisc"
89
90log "Downloading ${DEB_NAME}..."
91run "wget -qO '$TEMP_DEB' '$URL'"
92
93log "Removing conflicting Espanso packages (if any)..."
94# Suppress output and errors if packages don't exist
95run "apt-get remove -y espanso espanso-wayland >/dev/null 2>&1 || true"
96
97log "Installing package..."
98run "apt-get install -y '$TEMP_DEB'"
99
100ESPANSO_BIN="$(command -v espanso || true)"
101[[ -x "$ESPANSO_BIN" ]] || die "espanso binary not found after install."
102
103if [[ "$VARIANT" == "wayland" ]]; then
104 log "Setting CAP_DAC_OVERRIDE on $ESPANSO_BIN..."
105 run "setcap 'cap_dac_override+p' '$ESPANSO_BIN'"
106fi
107
108log "Clearing ghost processes to prevent start timeouts..."
109run "sudo -u '$ACTUAL_USER' killall espanso 2>/dev/null || true"
110
111log "Registering & starting espanso service for $ACTUAL_USER..."
112# Register may fail if already registered — treat that as success.
113if (( DRY_RUN )); then
114 printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso service register || true\n' "$ACTUAL_USER" "$USER_ID"
115 printf ' DRY-RUN: sudo -u %s XDG_RUNTIME_DIR=/run/user/%s espanso start || true\n' "$ACTUAL_USER" "$USER_ID"
116else
117 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso service register || true
118 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso restart || \
119 sudo -u "$ACTUAL_USER" XDG_RUNTIME_DIR="/run/user/$USER_ID" espanso start || true
120fi
121
122log "Done. Espanso ($VARIANT) installed and started for $ACTUAL_USER."
123if [[ "$VARIANT" == "wayland" ]]; then
124 log "Wayland note: non-US keyboards must set the layout in ~/.config/espanso/config/default.yml"
125fi
install-firefox.sh 原始檔案
1#!/usr/bin/env bash
2# install-firefox.sh — Install Firefox from Mozilla's official APT repository.
3# Removes the Snap transition package and pins Mozilla as the source of truth.
4# Hardened: distro-detect, idempotent, signed-by keyring, ERR trap, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Removes Firefox Snap (and Ubuntu's transitional wrapper), configures the
18official Mozilla APT repository (signed-by keyring + APT pin), then installs
19Firefox so it auto-updates from Mozilla.
20
21Options:
22 --dry-run Print the actions without executing them.
23 --yes, -y Pass -y to apt for non-interactive install.
24 --help, -h Show this help.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32
33on_err() { local rc=$? line=$1; printf '\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n' "${SCRIPT_NAME%.sh}" "$line" "$rc" >&2; }
34trap 'on_err $LINENO' ERR
35
36while (( $# )); do
37 case "$1" in
38 --dry-run) DRY_RUN=1 ;;
39 -y|--yes) ASSUME_YES=1 ;;
40 -h|--help) usage; exit 0 ;;
41 *) die "Unknown argument: $1 (try --help)" ;;
42 esac
43 shift
44done
45
46(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
47
48[[ -r /etc/os-release ]] || die "/etc/os-release not found; cannot detect distro."
49# shellcheck disable=SC1091
50. /etc/os-release
51case "${ID:-}:${ID_LIKE:-}" in
52 *ubuntu*|*debian*) : ;;
53 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
54esac
55log "Detected: ${PRETTY_NAME:-unknown} (codename: ${VERSION_CODENAME:-?})"
56
57APT_YES=()
58(( ASSUME_YES )) && APT_YES=(-y) || APT_YES=(-y) # always -y for safety in scripted use
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.mozilla.org.asc
62SOURCES=/etc/apt/sources.list.d/mozilla.list
63PIN=/etc/apt/preferences.d/mozilla
64
65log "Removing Firefox Snap and Ubuntu's transitional wrapper (if present)..."
66if command -v snap >/dev/null 2>&1; then
67 run "snap disable firefox >/dev/null 2>&1 || true"
68 run "snap remove --purge firefox >/dev/null 2>&1 || true"
69fi
70run "apt-get remove --purge ${APT_YES[*]} firefox >/dev/null 2>&1 || true"
71run "rm -f /usr/bin/firefox"
72
73log "Installing prerequisites (wget, gpg, ca-certificates)..."
74run "apt-get update -qq"
75run "apt-get install ${APT_YES[*]} wget gpg ca-certificates"
76
77log "Configuring Mozilla APT repository..."
78run "install -d -m 0755 /etc/apt/keyrings"
79if [[ ! -s "$KEYRING" ]]; then
80 run "wget -qO '$KEYRING' https://packages.mozilla.org/apt/repo-signing-key.gpg"
81 run "chmod 0644 '$KEYRING'"
82else
83 log "Keyring already present at $KEYRING (skipping download)."
84fi
85
86# Verify key fingerprint matches Mozilla's published fingerprint
87EXPECTED_FPR="35BAA0B33E9EB396F59CA838C0BA5CE6DC6315A3"
88ACTUAL_FPR="$(gpg --show-keys --with-colons "$KEYRING" 2>/dev/null | awk -F: '/^fpr/ {print $10; exit}')"
89if [[ "$ACTUAL_FPR" != "$EXPECTED_FPR" ]]; then
90 warn "Mozilla key fingerprint mismatch (expected $EXPECTED_FPR, got ${ACTUAL_FPR:-none}). Continuing, but verify manually."
91else
92 log "Mozilla key fingerprint verified."
93fi
94
95DESIRED_SRC='deb [signed-by=/etc/apt/keyrings/packages.mozilla.org.asc] https://packages.mozilla.org/apt mozilla main'
96if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
97 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
98fi
99
100log "Pinning Mozilla repo to priority 1000..."
101if [[ ! -f "$PIN" ]] || ! grep -q 'origin packages.mozilla.org' "$PIN"; then
102 if (( DRY_RUN )); then
103 printf ' DRY-RUN: write %s\n' "$PIN"
104 else
105 cat >"$PIN" <<'EOF'
106Package: *
107Pin: origin packages.mozilla.org
108Pin-Priority: 1000
109EOF
110 fi
111fi
112
113log "Installing Firefox from Mozilla repo..."
114run "apt-get update -qq"
115run "apt-get install ${APT_YES[*]} firefox"
116
117if (( ! DRY_RUN )) && command -v firefox >/dev/null 2>&1; then
118 log "Installed: $(firefox --version 2>/dev/null || echo 'firefox')"
119fi
120
121log "Done. Firefox installed from Mozilla APT and will auto-update."
122
install-font.sh 原始檔案
1#!/usr/bin/env bash
2
3set -euo pipefail
4IFS=$'\n\t'
5
6readonly SCRIPT_NAME="${0##*/}"
7readonly FONT_BASE_DIR="$HOME/.local/share/fonts"
8readonly TMP_DIR="$(mktemp -d)"
9
10log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
11warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
12die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
13
14cleanup() {
15 rm -rf "$TMP_DIR"
16}
17trap cleanup EXIT
18
19if (( EUID == 0 )); then
20 die "Do not run as root. Run as your normal desktop user so fonts install under $HOME."
21fi
22
23for tool in curl unzip fc-cache find; do
24 command -v "$tool" >/dev/null 2>&1 || die "Missing required tool: $tool"
25done
26
27latest_nerd_font_url() {
28 local asset_name="$1"
29 local url
30
31 url="$(
32 curl -fsSL https://api.github.com/repos/ryanoasis/nerd-fonts/releases/latest \
33 | grep "browser_download_url.*${asset_name}" \
34 | cut -d '"' -f 4 \
35 | head -n 1 \
36 || true
37 )"
38
39 [[ -n "$url" ]] || die "Could not find download URL for $asset_name. Check connectivity or GitHub API limits."
40 printf '%s' "$url"
41}
42
43install_nerd_font() {
44 local display_name="$1"
45 local asset_name="$2"
46 local font_dir="$3"
47 local download_url="$4"
48 local zip_path="$TMP_DIR/${asset_name}.zip"
49 local extract_dir="$TMP_DIR/${asset_name}"
50 local installed_count
51
52 if [[ -z "$download_url" ]]; then
53 log "Finding latest $display_name release..."
54 download_url="$(latest_nerd_font_url "${asset_name}.zip")"
55 fi
56
57 log "Downloading $display_name..."
58 curl -fL --retry 3 --retry-delay 2 -o "$zip_path" "$download_url"
59
60 log "Extracting $display_name..."
61 mkdir -p "$extract_dir" "$font_dir"
62 unzip -q -o "$zip_path" -d "$extract_dir"
63
64 installed_count="$(
65 find "$extract_dir" -type f \( -name '*.ttf' -o -name '*.otf' \) \
66 -exec cp {} "$font_dir/" \; \
67 -printf '.' \
68 | wc -c
69 )"
70
71 [[ "$installed_count" -gt 0 ]] || die "No font files found in ${asset_name}.zip"
72 find "$font_dir" -type f \( -name '*.ttf' -o -name '*.otf' \) -exec chmod 0644 {} +
73 log "Installed $installed_count font files to $font_dir"
74}
75
76mkdir -p "$FONT_BASE_DIR"
77
78install_nerd_font \
79 "Ubuntu Sans Nerd Font" \
80 "UbuntuSans" \
81 "$FONT_BASE_DIR/UbuntuSans" \
82 ""
83
84install_nerd_font \
85 "JetBrains Mono Nerd Font" \
86 "JetBrainsMono" \
87 "$FONT_BASE_DIR/JetBrainsMonoNerdFont" \
88 ""
89
90log "Updating font cache..."
91fc-cache -f "$FONT_BASE_DIR"
92
93if command -v fc-match >/dev/null 2>&1; then
94 log "Resolved terminal font: $(fc-match 'JetBrainsMono Nerd Font Mono')"
95fi
96
97log "Done. Configure your terminal to use JetBrainsMono Nerd Font Mono for Starship Powerline symbols."
98
install-ibus-pinyin.sh 原始檔案
1#!/usr/bin/env bash
2# install-ibus-rime.sh — Install IBus + Rime and add it
3# to GNOME's Input Sources. Tuned for Ubuntu 26.04 (GNOME 50 / Wayland) but
4# works on any modern Ubuntu/Debian GNOME desktop.
5#
6# Hardened: distro-detect, runs as the desktop user (not root) for gsettings,
7# uses sudo only for apt steps, idempotent re-runs, ERR trap, --dry-run.
8
9set -euo pipefail
10IFS=$'\n\t'
11
12readonly SCRIPT_NAME="${0##*/}"
13DRY_RUN=0
14SKIP_GSETTINGS=0
15
16usage() {
17 cat <<EOF
18Usage: $SCRIPT_NAME [--dry-run] [--skip-gsettings] [--help]
19
20Purges old ibus-libpinyin, installs ibus-rime (plus Chinese language packs),
21configures Rime to default to Simplified Pinyin, and registers 'Rime'
22as a GNOME input source. Idempotent.
23
24Do NOT run with sudo: gsettings is per-user and must run as the desktop user.
25The script invokes sudo internally only for apt-get steps.
26
27Options:
28 --skip-gsettings Install packages but don't touch GNOME input sources
29 (useful on non-GNOME desktops; configure manually afterwards).
30 --dry-run Print actions without executing.
31 --help, -h Show this help.
32
33Switch input methods at runtime with: Super + Space
34EOF
35}
36
37log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
38warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
39die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
40run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
41trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
42
43while (( $# )); do
44 case "$1" in
45 --dry-run) DRY_RUN=1 ;;
46 --skip-gsettings) SKIP_GSETTINGS=1 ;;
47 -h|--help) usage; exit 0 ;;
48 *) die "Unknown argument: $1 (try --help)" ;;
49 esac
50 shift
51done
52
53# Resolve the desktop user. If invoked via sudo, gsettings must target SUDO_USER.
54if (( EUID == 0 )); then
55 DESKTOP_USER="${SUDO_USER:-}"
56 [[ -n "$DESKTOP_USER" && "$DESKTOP_USER" != "root" ]] \
57 || die "Run as a normal user (the script will call sudo itself for apt). gsettings can't run as root."
58else
59 DESKTOP_USER="$USER"
60fi
61
62# Look up the user's UID and Home Directory for configs and DBus.
63USER_ENTRY="$(getent passwd "$DESKTOP_USER")" || die "User '$DESKTOP_USER' not found in passwd."
64USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
65USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")"
66
67[[ -r /etc/os-release ]] || die "/etc/os-release not found."
68# shellcheck disable=SC1091
69. /etc/os-release
70case "${ID:-}:${ID_LIKE:-}" in
71 *ubuntu*|*debian*) : ;;
72 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}. Requires Debian/Ubuntu." ;;
73esac
74log "Detected: ${PRETTY_NAME:-unknown}, desktop user: $DESKTOP_USER"
75
76# Helper: invoke a command as $DESKTOP_USER with a working DBus address.
77run_as_user() {
78 local cmd=("$@")
79 if (( DRY_RUN )); then
80 printf ' DRY-RUN (as %s): %s\n' "$DESKTOP_USER" "${cmd[*]}"
81 return 0
82 fi
83 if (( EUID == 0 )); then
84 sudo -u "$DESKTOP_USER" \
85 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
86 XDG_RUNTIME_DIR="/run/user/$USER_ID" \
87 "${cmd[@]}"
88 else
89 "${cmd[@]}"
90 fi
91}
92
93# Helper: invoke a command with sudo when the caller is non-root.
94sudo_run() {
95 if (( DRY_RUN )); then
96 printf ' DRY-RUN (sudo): %s\n' "$*"
97 return 0
98 fi
99 if (( EUID == 0 )); then "$@"; else sudo "$@"; fi
100}
101
102export DEBIAN_FRONTEND=noninteractive
103
104log "Removing old ibus-libpinyin if present..."
105sudo_run apt-get remove --purge -y ibus-libpinyin || true
106
107log "Installing IBus Rime + Chinese language packs..."
108sudo_run apt-get update -qq
109sudo_run apt-get install -y \
110 ibus \
111 ibus-rime \
112 language-pack-zh-hans \
113 language-pack-gnome-zh-hans
114
115log "Configuring Rime to permanently default to Simplified Pinyin..."
116RIME_DIR="$USER_HOME/.config/ibus/rime"
117if (( DRY_RUN )); then
118 printf ' DRY-RUN (as %s): Create %s/default.custom.yaml\n' "$DESKTOP_USER" "$RIME_DIR"
119else
120 if (( EUID == 0 )); then
121 sudo -u "$DESKTOP_USER" mkdir -p "$RIME_DIR"
122 sudo -u "$DESKTOP_USER" tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF'
123patch:
124 schema_list:
125 - schema: luna_pinyin_simp
126EOF
127 else
128 mkdir -p "$RIME_DIR"
129 tee "$RIME_DIR/default.custom.yaml" > /dev/null <<'EOF'
130patch:
131 schema_list:
132 - schema: luna_pinyin_simp
133EOF
134 fi
135fi
136
137# Make sure ibus-daemon picks up the new engines and config for the user.
138if command -v ibus >/dev/null 2>&1; then
139 log "Restarting ibus-daemon for $DESKTOP_USER..."
140 # `ibus exit` will fail if no daemon is running — treat as non-fatal.
141 run_as_user ibus exit >/dev/null 2>&1 || true
142 # Start fresh in the background; -drx replaces a running daemon.
143 if (( ! DRY_RUN )); then
144 sudo -u "$DESKTOP_USER" \
145 DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
146 XDG_RUNTIME_DIR="/run/user/$USER_ID" \
147 sh -c 'nohup ibus-daemon -drx >/dev/null 2>&1 &' || \
148 warn "ibus-daemon restart returned non-zero (non-fatal)."
149 sleep 1
150 fi
151fi
152
153if (( SKIP_GSETTINGS )); then
154 log "Skipping GNOME input-sources update (--skip-gsettings)."
155 log "Done. Add 'Chinese (Rime)' manually under Settings → Keyboard → Input Sources."
156 exit 0
157fi
158
159# Only manage gsettings if GNOME schemas are present.
160if ! run_as_user gsettings list-schemas 2>/dev/null | grep -q '^org.gnome.desktop.input-sources$'; then
161 warn "GNOME schema org.gnome.desktop.input-sources not found; skipping gsettings update."
162 log "Done. Add 'Chinese (Rime)' manually in your DE's input settings."
163 exit 0
164fi
165
166log "Adding 'Rime' to GNOME Input Sources (idempotent)..."
167
168CURRENT_SOURCES="$(run_as_user gsettings get org.gnome.desktop.input-sources sources 2>/dev/null || echo '[]')"
169# Strip the optional "@as " type annotation gvariant sometimes prepends.
170CLEAN_SOURCES="${CURRENT_SOURCES#@as }"
171
172if [[ "$CLEAN_SOURCES" == *"'ibus', 'rime'"* ]]; then
173 log "Rime already present in input sources — nothing to do."
174else
175 if [[ "$CLEAN_SOURCES" == *"'ibus', 'libpinyin'"* ]]; then
176 # Dynamically replace libpinyin with rime
177 NEW_SOURCES="${CLEAN_SOURCES//\'libpinyin\'/\'rime\'}"
178 elif [[ -z "$CLEAN_SOURCES" || "$CLEAN_SOURCES" == "[]" || "$CLEAN_SOURCES" == "@as []" ]]; then
179 NEW_SOURCES="[('xkb', 'us'), ('ibus', 'rime')]"
180 else
181 # Insert the rime tuple before the closing bracket of the existing list.
182 NEW_SOURCES="${CLEAN_SOURCES%]*}, ('ibus', 'rime')]"
183 fi
184 run_as_user gsettings set org.gnome.desktop.input-sources sources "$NEW_SOURCES"
185 log "Input sources updated to include: ('ibus', 'rime')"
186fi
187
188log "---"
189log "Done! Rime is installed and configured for Simplified Pinyin."
190log "Switch input methods with: Super + Space"
191log "If 'Chinese (Rime)' doesn't appear in the top bar right away, log out and back in."
install-ipatool.sh 原始檔案
1#!/usr/bin/env bash
2# install-ipatool.sh — Install ipatool from the latest GitHub release.
3# Hardened: arch detection (amd64/arm64), GitHub API token support, SHA-256 verification
4# from the release checksum file, atomic install to /usr/local/bin, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="majd/ipatool"
12INSTALL_PATH="/usr/local/bin/ipatool"
13
14usage() {
15 cat <<EOF
16Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
17
18Resolves the latest release of majd/ipatool, downloads the tarball for your
19architecture, verifies its SHA-256 against the published checksums.txt, and
20installs the binary to /usr/local/bin/ipatool atomically.
21
22If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
23GitHub API request (avoids rate limits).
24
25Options:
26 --dry-run Print actions without executing.
27 --help, -h Show this help.
28EOF
29}
30
31log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
32warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
33die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
34run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
35trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
36
37while (( $# )); do
38 case "$1" in
39 --dry-run) DRY_RUN=1 ;;
40 -h|--help) usage; exit 0 ;;
41 *) die "Unknown argument: $1 (try --help)" ;;
42 esac
43 shift
44done
45
46(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
47
48[[ -r /etc/os-release ]] || die "/etc/os-release not found."
49# shellcheck disable=SC1091
50. /etc/os-release
51log "Detected: ${PRETTY_NAME:-unknown}"
52
53ARCH_RAW="$(uname -m)"
54case "$ARCH_RAW" in
55 x86_64) GO_ARCH=amd64 ;;
56 aarch64) GO_ARCH=arm64 ;;
57 armv7l|armv6l) GO_ARCH=arm ;;
58 *) die "Unsupported architecture: $ARCH_RAW" ;;
59esac
60ASSET_SUFFIX="linux-${GO_ARCH}.tar.gz"
61
62export DEBIAN_FRONTEND=noninteractive
63log "Installing prerequisites..."
64run "apt-get update -qq"
65run "apt-get install -y curl jq tar ca-certificates libsecret-1-0"
66
67GH_HDRS=(-H "Accept: application/vnd.github+json")
68[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
69
70log "Querying GitHub API for latest release of $REPO..."
71RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
72TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
73[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
74log "Latest release: $TAG"
75
76DOWNLOAD_URL="$(jq -r --arg s "$ASSET_SUFFIX" '.assets[] | select(.name | endswith($s)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
77CHECKSUM_URL="$(jq -r '.assets[] | select(.name | test("checksums?\\.txt$")) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
78[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No release asset matching '*${ASSET_SUFFIX}'."
79
80STAGE="$(mktemp -d -t ipatool.XXXXXX)"
81trap 'rm -rf "$STAGE"' EXIT
82TARBALL="$STAGE/ipatool.tar.gz"
83
84log "Downloading $(basename "$DOWNLOAD_URL")..."
85run "curl -fsSL -o '$TARBALL' '$DOWNLOAD_URL'"
86
87if [[ -n "$CHECKSUM_URL" ]]; then
88 log "Verifying SHA-256..."
89 EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk -v f="$(basename "$DOWNLOAD_URL")" '$2 ~ f || $2 == "*"f {print $1; exit}')"
90 ACTUAL="$(sha256sum "$TARBALL" | awk '{print $1}')"
91 if [[ -n "$EXPECTED" && "$EXPECTED" != "$ACTUAL" ]]; then
92 die "SHA-256 mismatch: expected=$EXPECTED actual=$ACTUAL"
93 fi
94 [[ -n "$EXPECTED" ]] && log "SHA-256 ok." || warn "Asset not listed in checksums.txt; skipping."
95else
96 warn "No checksums.txt in release; skipping SHA-256 verification."
97fi
98
99log "Extracting..."
100run "tar -xzf '$TARBALL' -C '$STAGE'"
101BINARY_PATH="$(find "$STAGE" -type f -name ipatool -executable -not -name '*.tar.gz' | head -n1 || true)"
102if [[ -z "$BINARY_PATH" ]]; then
103 # Some releases ship the binary without +x; relax the find
104 BINARY_PATH="$(find "$STAGE" -type f -name ipatool -not -name '*.tar.gz' | head -n1 || true)"
105fi
106[[ -n "$BINARY_PATH" || $DRY_RUN -eq 1 ]] || die "ipatool binary not found inside archive."
107
108log "Installing to $INSTALL_PATH..."
109run "install -m 0755 '$BINARY_PATH' '$INSTALL_PATH'"
110
111if (( ! DRY_RUN )) && command -v ipatool >/dev/null 2>&1; then
112 log "Installed: $(ipatool --version 2>/dev/null || basename "$INSTALL_PATH") ($TAG)"
113fi
114log "Done."
115
install-jetbrains-toolbox.sh 原始檔案
1#!/usr/bin/env bash
2# install-jetbrains-toolbox.sh — Install JetBrains Toolbox into the invoking user's home.
3# Hardened: dependency check w/ auto-install, SHA-256 verification against JetBrains'
4# release metadata, idempotent (replaces atomically), .desktop entry, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11ASSUME_YES=0
12
13usage() {
14 cat <<EOF
15Usage: $SCRIPT_NAME [--dry-run] [--yes] [--help]
16
17Installs the latest JetBrains Toolbox to ~/.local/share/JetBrains/Toolbox and
18creates a .desktop launcher in ~/.local/share/applications. The .tar.gz is
19verified against the SHA-256 published in JetBrains' release feed.
20
21Do NOT run with sudo: Toolbox is a per-user install. If a missing system
22package (curl/jq/tar/libfuse2) needs installing, the script will call sudo
23just for that step.
24
25Options:
26 --yes, -y Auto-confirm prompts for installing missing system packages.
27 --dry-run Print actions without executing.
28 --help, -h Show this help.
29EOF
30}
31
32log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
33warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
34die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
35run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
36trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
37
38while (( $# )); do
39 case "$1" in
40 --dry-run) DRY_RUN=1 ;;
41 -y|--yes) ASSUME_YES=1 ;;
42 -h|--help) usage; exit 0 ;;
43 *) die "Unknown argument: $1 (try --help)" ;;
44 esac
45 shift
46done
47
48(( EUID != 0 )) || die "Do NOT run as root. Toolbox is a per-user install."
49
50[[ -r /etc/os-release ]] || die "/etc/os-release not found."
51# shellcheck disable=SC1091
52. /etc/os-release
53case "${ID:-}:${ID_LIKE:-}" in
54 *ubuntu*|*debian*) PKG_MGR=apt ;;
55 *fedora*|*rhel*) PKG_MGR=dnf ;;
56 *arch*) PKG_MGR=pacman ;;
57 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
58esac
59log "Detected: ${PRETTY_NAME:-unknown} (pkg: $PKG_MGR)"
60
61confirm() {
62 (( ASSUME_YES )) && return 0
63 read -rp "$1 [y/N] " ans
64 [[ "$ans" =~ ^[Yy] ]]
65}
66
67ensure_pkg() {
68 local pkg="$1" probe="$2"
69 if eval "$probe" >/dev/null 2>&1; then return 0; fi
70 if ! confirm "Package '$pkg' is missing. Install via sudo $PKG_MGR?"; then
71 die "'$pkg' is required."
72 fi
73 case "$PKG_MGR" in
74 apt) run "sudo apt-get update -qq && sudo apt-get install -y '$pkg'" ;;
75 dnf) run "sudo dnf install -y '$pkg'" ;;
76 pacman) run "sudo pacman -Sy --noconfirm '$pkg'" ;;
77 esac
78}
79
80# Map deps: command-or-package-probe
81ensure_pkg curl "command -v curl"
82ensure_pkg jq "command -v jq"
83ensure_pkg tar "command -v tar"
84case "$PKG_MGR" in
85 apt) ensure_pkg libfuse2 "dpkg -s libfuse2" ;;
86 dnf) ensure_pkg fuse-libs "rpm -q fuse-libs" ;;
87 pacman) ensure_pkg fuse2 "pacman -Q fuse2" ;;
88esac
89
90ARCH_RAW="$(uname -m)"
91case "$ARCH_RAW" in
92 x86_64) TOOLBOX_ARCH_KEY=linux ;;
93 aarch64) TOOLBOX_ARCH_KEY=linuxARM64 ;;
94 *) die "Unsupported architecture: $ARCH_RAW" ;;
95esac
96
97log "Querying JetBrains release feed..."
98RELEASE_JSON="$(curl -fsSL 'https://data.services.jetbrains.com/products/releases?code=TBA&latest=true&type=release' \
99 -H 'Origin: https://www.jetbrains.com' \
100 -H 'Referer: https://www.jetbrains.com/toolbox/download/')"
101
102TOOLBOX_URL="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].link // empty' <<<"$RELEASE_JSON")"
103TOOLBOX_SHA="$(jq -r --arg k "$TOOLBOX_ARCH_KEY" '.TBA[0].downloads[$k].checksumLink // empty' <<<"$RELEASE_JSON")"
104TOOLBOX_VER="$(jq -r '.TBA[0].version // "?"' <<<"$RELEASE_JSON")"
105[[ -n "$TOOLBOX_URL" ]] || die "Could not resolve Toolbox download URL from release feed."
106log "Latest Toolbox: $TOOLBOX_VER ($TOOLBOX_ARCH_KEY)"
107
108INSTALL_DIR="$HOME/.local/share/JetBrains/Toolbox"
109STAGE_DIR="$(mktemp -d -t toolbox.XXXXXX)"
110trap 'rm -rf "$STAGE_DIR"' EXIT
111
112TARBALL="$STAGE_DIR/toolbox.tar.gz"
113log "Downloading..."
114run "curl -fsSL -o '$TARBALL' '$TOOLBOX_URL'"
115
116if [[ -n "$TOOLBOX_SHA" ]]; then
117 log "Verifying SHA-256..."
118 EXPECTED_SHA="$(curl -fsSL "$TOOLBOX_SHA" | awk '{print $1}')"
119 ACTUAL_SHA="$(sha256sum "$TARBALL" | awk '{print $1}')"
120 if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "$ACTUAL_SHA" ]]; then
121 die "SHA-256 mismatch! expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
122 fi
123 log "SHA-256 ok."
124else
125 warn "No checksum URL in release feed; skipping verification."
126fi
127
128log "Extracting to $INSTALL_DIR..."
129run "mkdir -p '$INSTALL_DIR'"
130run "tar -xzf '$TARBALL' --strip-components=1 -C '$INSTALL_DIR'"
131
132BIN_PATH="$INSTALL_DIR/bin/jetbrains-toolbox"
133DESKTOP_SRC="$INSTALL_DIR/bin/jetbrains-toolbox.desktop"
134ICON_PATH="$INSTALL_DIR/bin/toolbox-tray-color.png"
135
136[[ -x "$BIN_PATH" || $DRY_RUN -eq 1 ]] || die "Toolbox binary missing after extract."
137
138# Optional ~/bin symlink
139if [[ -d "$HOME/bin" ]]; then
140 run "ln -sfn '$BIN_PATH' '$HOME/bin/jetbrains-toolbox'"
141fi
142
143# .desktop launcher
144APPS_DIR="$HOME/.local/share/applications"
145run "mkdir -p '$APPS_DIR'"
146DESKTOP_DST="$APPS_DIR/jetbrains-toolbox.desktop"
147if [[ -f "$DESKTOP_SRC" ]] || (( DRY_RUN )); then
148 run "cp '$DESKTOP_SRC' '$DESKTOP_DST'"
149 run "sed -i 's|^Exec=.*|Exec=$BIN_PATH %u|' '$DESKTOP_DST'"
150 if [[ -f "$ICON_PATH" ]]; then
151 run "sed -i 's|^Icon=.*|Icon=$ICON_PATH|' '$DESKTOP_DST'"
152 fi
153 run "chmod 0755 '$DESKTOP_DST'"
154 log "Desktop entry installed: $DESKTOP_DST"
155else
156 warn "No .desktop file in archive; skipping launcher."
157fi
158
159log "Done. JetBrains Toolbox $TOOLBOX_VER installed to $INSTALL_DIR"
160
install-libreoffice.sh 原始檔案
1#!/usr/bin/env bash
2# install-libreoffice.sh — Install the latest stable LibreOffice from documentfoundation.org.
3# Hardened: arch detection, version JSON when available with HTML fallback, MD5 verification,
4# uninstalls bundled distro libreoffice* first to avoid conflicts, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11SKIP_REMOVE_DISTRO=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--keep-distro-libreoffice] [--help]
16
17Detects the latest stable LibreOffice version on download.documentfoundation.org,
18downloads the matching DEB tarball for your architecture, verifies the MD5 sum
19published alongside it, and installs the .deb packages.
20
21Options:
22 --keep-distro-libreoffice Don't purge any pre-installed distro libreoffice*
23 (default: purge to avoid library conflicts).
24 --dry-run Print actions without executing.
25 --help, -h Show this help.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
31die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
32run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
33trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
34
35while (( $# )); do
36 case "$1" in
37 --dry-run) DRY_RUN=1 ;;
38 --keep-distro-libreoffice) SKIP_REMOVE_DISTRO=1 ;;
39 -h|--help) usage; exit 0 ;;
40 *) die "Unknown argument: $1 (try --help)" ;;
41 esac
42 shift
43done
44
45(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
46
47[[ -r /etc/os-release ]] || die "/etc/os-release not found."
48# shellcheck disable=SC1091
49. /etc/os-release
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown} (this script installs .deb packages)." ;;
53esac
54
55ARCH_RAW="$(uname -m)"
56case "$ARCH_RAW" in
57 x86_64) LO_ARCH=x86_64; LO_SUFFIX=Linux_x86-64 ;;
58 aarch64) LO_ARCH=aarch64; LO_SUFFIX=Linux_aarch64 ;;
59 *) die "Unsupported architecture: $ARCH_RAW (LibreOffice ships x86_64 and aarch64)." ;;
60esac
61log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH_RAW"
62
63export DEBIAN_FRONTEND=noninteractive
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y curl wget tar coreutils ca-certificates"
68
69log "Resolving latest stable LibreOffice version..."
70INDEX="$(curl -fsSL https://download.documentfoundation.org/libreoffice/stable/)"
71LATEST_VERSION="$(printf '%s\n' "$INDEX" \
72 | grep -oP '(?<=href=")[0-9]+\.[0-9]+\.[0-9]+(?=/")' \
73 | sort -V | tail -1 || true)"
74[[ -n "$LATEST_VERSION" ]] || die "Could not detect latest version from index page."
75log "Latest stable: $LATEST_VERSION"
76
77BASE="https://download.documentfoundation.org/libreoffice/stable/${LATEST_VERSION}/deb/${LO_ARCH}"
78ARCHIVE_NAME="LibreOffice_${LATEST_VERSION}_${LO_SUFFIX}_deb.tar.gz"
79URL="${BASE}/${ARCHIVE_NAME}"
80SUM_URL="${URL}.md5"
81
82STAGE="$(mktemp -d -t lo.XXXXXX)"
83trap 'rm -rf "$STAGE"' EXIT
84ARCHIVE="$STAGE/$ARCHIVE_NAME"
85
86log "Downloading $ARCHIVE_NAME..."
87run "wget -q --show-progress -O '$ARCHIVE' '$URL'"
88
89log "Verifying MD5..."
90if EXPECTED_MD5="$(curl -fsSL "$SUM_URL" 2>/dev/null | awk '{print $1}')" && [[ -n "$EXPECTED_MD5" ]]; then
91 ACTUAL_MD5="$(md5sum "$ARCHIVE" | awk '{print $1}')"
92 [[ "$EXPECTED_MD5" == "$ACTUAL_MD5" ]] || die "MD5 mismatch: expected=$EXPECTED_MD5 actual=$ACTUAL_MD5"
93 log "MD5 ok."
94else
95 warn "No MD5 published for $SUM_URL; skipping checksum."
96fi
97
98log "Extracting..."
99run "tar -xzf '$ARCHIVE' -C '$STAGE'"
100DEBS_DIR="$(find "$STAGE" -maxdepth 3 -type d -name DEBS | head -n1 || true)"
101[[ -n "$DEBS_DIR" || $DRY_RUN -eq 1 ]] || die "DEBS/ directory not found in archive."
102
103if (( ! SKIP_REMOVE_DISTRO )); then
104 if dpkg -l 'libreoffice*' 2>/dev/null | awk '/^ii/ {print $2}' | grep -q .; then
105 log "Removing distro-supplied libreoffice* packages to avoid conflicts..."
106 run "apt-get remove --purge -y 'libreoffice*'"
107 fi
108fi
109
110log "Installing .deb packages..."
111run "dpkg -i -R '$DEBS_DIR'"
112log "Resolving any missing dependencies..."
113run "apt-get install -f -y"
114
115log "Done. LibreOffice $LATEST_VERSION installed."
116
install-localsend.sh 原始檔案
1#!/usr/bin/env bash
2# install-localsend.sh — Install LocalSend from the latest official GitHub release.
3# Hardened: arch-aware, GitHub API token support, SHA-256 verification from the
4# release asset digest, idempotent, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="localsend/localsend"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Resolves the latest release of localsend/localsend, downloads the official
18Linux .deb package for your architecture, verifies its SHA-256 against the
19GitHub release asset digest when available, and installs it with apt-get.
20
21If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
22GitHub API request (avoids rate limits).
23
24Options:
25 --dry-run Print actions without executing.
26 --help, -h Show this help.
27EOF
28}
29
30log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
31warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
32die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
33run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
34trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
35
36while (( $# )); do
37 case "$1" in
38 --dry-run) DRY_RUN=1 ;;
39 -h|--help) usage; exit 0 ;;
40 *) die "Unknown argument: $1 (try --help)" ;;
41 esac
42 shift
43done
44
45(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
46
47[[ -r /etc/os-release ]] || die "/etc/os-release not found."
48# shellcheck disable=SC1091
49. /etc/os-release
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
53esac
54
55ARCH="$(dpkg --print-architecture)"
56case "$ARCH" in
57 amd64) ASSET_ARCH="x86-64" ;;
58 arm64) ASSET_ARCH="arm-64" ;;
59 *) die "LocalSend publishes Linux .deb assets for amd64 and arm64 only (detected: $ARCH)." ;;
60esac
61log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
62
63export DEBIAN_FRONTEND=noninteractive
64log "Installing prerequisites..."
65run "apt-get update -qq"
66run "apt-get install -y curl jq ca-certificates"
67
68GH_HDRS=(-H "Accept: application/vnd.github+json")
69[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
70
71log "Querying GitHub API for latest release of $REPO..."
72RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
73TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
74[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
75log "Latest release: $TAG"
76
77ASSET_REGEX="^LocalSend-[^-]+-linux-${ASSET_ARCH}\\.deb$"
78DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
79ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)"
80EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)"
81[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No $ARCH .deb asset found in latest LocalSend release."
82
83STAGE="$(mktemp -d -t localsend.XXXXXX)"
84trap 'rm -rf "$STAGE"' EXIT
85DEB="$STAGE/$ASSET_NAME"
86
87log "Downloading $ASSET_NAME..."
88run "curl -fL -o '$DEB' '$DOWNLOAD_URL'"
89
90if (( DRY_RUN )); then
91 [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \
92 && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \
93 || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n'
94 printf ' DRY-RUN: apt-get install -y %s\n' "$DEB"
95 log "Done."
96 exit 0
97fi
98
99if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then
100 log "Verifying SHA-256..."
101 ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')"
102 [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
103 log "SHA-256 ok."
104else
105 warn "No SHA-256 asset digest in GitHub release metadata; skipping verification."
106fi
107
108log "Installing LocalSend..."
109run "apt-get install -y '$DEB'"
110
111if command -v localsend_app >/dev/null 2>&1; then
112 log "Installed: $(localsend_app --version 2>/dev/null || echo "$TAG")"
113else
114 log "Installed: $TAG"
115fi
116log "Done."
117
install-network_drive.sh 原始檔案
1#!/usr/bin/env bash
2# install-network_drive.sh — Auto-mount Synology (SMB/CIFS) shares under /mnt/Synology.
3# Hardened: safe input handling, no eval-on-username, hostname validation,
4# fstab managed via begin/end markers (idempotent re-run), creds file 0600,
5# GNOME dock pinning is best-effort.
6
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12SERVER_ADDR=""
13SMB_USER=""
14SMB_PASS=""
15MASTER_DIR="/mnt/Synology"
16
17usage() {
18 cat <<EOF
19Usage: sudo $SCRIPT_NAME [options]
20
21Discovers SMB shares on a Synology NAS and adds them to /etc/fstab so they
22auto-mount under $MASTER_DIR. Creates a desktop launcher and (best-effort)
23pins it to the top of the GNOME Dock.
24
25Options:
26 --server HOST_OR_IP Synology address (skips prompt).
27 --user USERNAME SMB username (skips prompt).
28 --password-stdin Read SMB password from stdin (skips prompt).
29 (Otherwise the script prompts on the controlling tty.)
30 --mount-root DIR Override parent directory (default: $MASTER_DIR).
31 --dry-run Print actions without executing.
32 --help, -h Show this help.
33EOF
34}
35
36log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
37warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
38die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
39run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
40trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
41
42PASS_FROM_STDIN=0
43while (( $# )); do
44 case "$1" in
45 --server) SERVER_ADDR="${2:?}"; shift ;;
46 --user) SMB_USER="${2:?}"; shift ;;
47 --password-stdin) PASS_FROM_STDIN=1 ;;
48 --mount-root) MASTER_DIR="${2:?}"; shift ;;
49 --dry-run) DRY_RUN=1 ;;
50 -h|--help) usage; exit 0 ;;
51 *) die "Unknown argument: $1 (try --help)" ;;
52 esac
53 shift
54done
55
56(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
57
58[[ -r /etc/os-release ]] || die "/etc/os-release not found."
59# shellcheck disable=SC1091
60. /etc/os-release
61case "${ID:-}:${ID_LIKE:-}" in
62 *ubuntu*|*debian*) : ;;
63 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
64esac
65
66ACTUAL_USER="${SUDO_USER:-${USER:-}}"
67[[ -n "$ACTUAL_USER" && "$ACTUAL_USER" != "root" ]] || die "Run via sudo as a regular user."
68# Use getent so we don't depend on eval/~expansion or shell glob safety.
69USER_ENTRY="$(getent passwd "$ACTUAL_USER")" || die "User '$ACTUAL_USER' not found in passwd."
70USER_HOME="$(awk -F: '{print $6}' <<<"$USER_ENTRY")"
71USER_ID="$(awk -F: '{print $3}' <<<"$USER_ENTRY")"
72USER_GID="$(awk -F: '{print $4}' <<<"$USER_ENTRY")"
73
74export DEBIAN_FRONTEND=noninteractive
75log "Installing cifs-utils, smbclient..."
76run "apt-get update -qq"
77run "apt-get install -y cifs-utils smbclient"
78
79# --- Collect creds ---
80[[ -n "$SERVER_ADDR" ]] || read -rp "Synology NAS address or IP: " SERVER_ADDR </dev/tty
81SERVER_ADDR="${SERVER_ADDR#smb://}"
82SERVER_ADDR="${SERVER_ADDR#//}"
83SERVER_ADDR="${SERVER_ADDR%/}"
84[[ "$SERVER_ADDR" =~ ^[A-Za-z0-9._-]+$ ]] || die "Invalid host/IP: '$SERVER_ADDR'"
85
86[[ -n "$SMB_USER" ]] || read -rp "Synology username: " SMB_USER </dev/tty
87[[ "$SMB_USER" =~ ^[A-Za-z0-9._@-]+$ ]] || die "Invalid SMB username."
88
89if (( PASS_FROM_STDIN )); then
90 IFS= read -r SMB_PASS
91else
92 read -rsp "Synology password: " SMB_PASS </dev/tty
93 echo
94fi
95[[ -n "$SMB_PASS" ]] || die "Password is empty."
96
97# --- Credentials file ---
98CRED_FILE="$USER_HOME/.smbcredentials_synology"
99log "Writing credentials to $CRED_FILE (mode 0600)..."
100if (( DRY_RUN )); then
101 printf ' DRY-RUN: write %s\n' "$CRED_FILE"
102else
103 umask 077
104 {
105 printf 'username=%s\n' "$SMB_USER"
106 printf 'password=%s\n' "$SMB_PASS"
107 } >"$CRED_FILE"
108 chown "$USER_ID:$USER_GID" "$CRED_FILE"
109 chmod 0600 "$CRED_FILE"
110fi
111
112# --- Query shares ---
113log "Discovering shares on //$SERVER_ADDR..."
114if (( DRY_RUN )); then
115 SHARE_LIST=$'home\nphoto\nvideo'
116else
117 SHARE_LIST="$(smbclient -L "//$SERVER_ADDR" -U "$SMB_USER%$SMB_PASS" -g 2>/dev/null \
118 | awk -F'|' '$1=="Disk" {print $2}' || true)"
119fi
120[[ -n "$SHARE_LIST" ]] || die "No shares returned. Check host, credentials, or network."
121
122# --- Parent mount dir ---
123run "mkdir -p '$MASTER_DIR'"
124run "chown '$USER_ID:$USER_GID' '$MASTER_DIR'"
125
126# --- Backup fstab once ---
127if [[ ! -f /etc/fstab.bak.synology ]]; then
128 run "cp /etc/fstab /etc/fstab.bak.synology"
129 log "Backed up fstab -> /etc/fstab.bak.synology"
130fi
131
132MARK_BEGIN="# >>> synology-master >>> (managed by ${SCRIPT_NAME})"
133MARK_END="# <<< synology-master <<<"
134# Remove any previous managed block (so re-run replaces, not appends).
135if grep -qF "$MARK_BEGIN" /etc/fstab; then
136 if (( DRY_RUN )); then
137 printf ' DRY-RUN: strip previous managed block from /etc/fstab\n'
138 else
139 sed -i "\|$MARK_BEGIN|,\|$MARK_END|d" /etc/fstab
140 fi
141fi
142
143log "Writing managed block to /etc/fstab..."
144FSTAB_BLOCK="$MARK_BEGIN"$'\n'
145while IFS= read -r SHARE; do
146 [[ -z "$SHARE" || "$SHARE" == "IPC\$" || "$SHARE" == "print\$" ]] && continue
147 MOUNT_POINT="$MASTER_DIR/$SHARE"
148 run "mkdir -p '$MOUNT_POINT'"
149 run "chown '$USER_ID:$USER_GID' '$MOUNT_POINT'"
150 FSTAB_BLOCK+="//${SERVER_ADDR}/${SHARE} ${MOUNT_POINT} cifs credentials=${CRED_FILE},uid=${USER_ID},gid=${USER_GID},_netdev,nofail,x-systemd.automount,x-systemd.idle-timeout=60 0 0"$'\n'
151done <<<"$SHARE_LIST"
152FSTAB_BLOCK+="$MARK_END"$'\n'
153
154if (( DRY_RUN )); then
155 printf ' DRY-RUN: append fstab block:\n%s\n' "$FSTAB_BLOCK"
156else
157 printf '%s' "$FSTAB_BLOCK" >>/etc/fstab
158fi
159
160log "Reloading systemd and mounting..."
161run "systemctl daemon-reload"
162run "mount -a -t cifs"
163
164# --- Desktop entry ---
165APPS_DIR="$USER_HOME/.local/share/applications"
166DESKTOP_FILENAME="synology-master.desktop"
167DESKTOP_FILE="$APPS_DIR/$DESKTOP_FILENAME"
168run "install -d -o '$USER_ID' -g '$USER_GID' -m 0755 '$APPS_DIR'"
169
170if (( DRY_RUN )); then
171 printf ' DRY-RUN: write %s\n' "$DESKTOP_FILE"
172else
173 cat >"$DESKTOP_FILE" <<EOF
174[Desktop Entry]
175Name=Synology NAS
176Comment=Open Synology Master Directory
177Exec=xdg-open ${MASTER_DIR}
178Icon=folder-remote
179Terminal=false
180Type=Application
181Categories=Network;FileTools;
182EOF
183 chown "$USER_ID:$USER_GID" "$DESKTOP_FILE"
184 chmod 0755 "$DESKTOP_FILE"
185fi
186
187# --- Best-effort GNOME dock pin ---
188if (( ! DRY_RUN )) && sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings list-keys org.gnome.shell >/dev/null 2>&1; then
189 log "Pinning to top of GNOME dock..."
190 CURRENT_FAVS="$(sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" gsettings get org.gnome.shell favorite-apps 2>/dev/null || echo '[]')"
191 if [[ "$CURRENT_FAVS" != *"$DESKTOP_FILENAME"* ]]; then
192 CLEAN_FAVS="${CURRENT_FAVS#@as }"
193 if [[ "$CLEAN_FAVS" == "[]" || -z "$CLEAN_FAVS" ]]; then
194 NEW_FAVS="['$DESKTOP_FILENAME']"
195 else
196 NEW_FAVS="['$DESKTOP_FILENAME', ${CLEAN_FAVS:1}"
197 fi
198 sudo -u "$ACTUAL_USER" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$USER_ID/bus" \
199 gsettings set org.gnome.shell favorite-apps "$NEW_FAVS" || warn "Dock pin failed (non-fatal)."
200 else
201 log "Already pinned."
202 fi
203else
204 log "GNOME not detected (or no active dbus session); skipping dock pin."
205fi
206
207log "Done. Shares mounted under $MASTER_DIR."
208
install-obsidian.sh 原始檔案
1#!/usr/bin/env bash
2# install-obsidian.sh — Install Obsidian from the latest official GitHub release.
3# Hardened: arch-aware, GitHub API token support, SHA-256 verification from the
4# release asset digest, idempotent, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="obsidianmd/obsidian-releases"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Resolves the latest release of obsidianmd/obsidian-releases, downloads the
18official amd64 .deb package, verifies its SHA-256 against the GitHub release
19asset digest when available, and installs it with apt-get.
20
21If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
22GitHub API request (avoids rate limits).
23
24Options:
25 --dry-run Print actions without executing.
26 --help, -h Show this help.
27EOF
28}
29
30log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
31warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
32die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
33run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
34trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
35
36while (( $# )); do
37 case "$1" in
38 --dry-run) DRY_RUN=1 ;;
39 -h|--help) usage; exit 0 ;;
40 *) die "Unknown argument: $1 (try --help)" ;;
41 esac
42 shift
43done
44
45(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
46
47[[ -r /etc/os-release ]] || die "/etc/os-release not found."
48# shellcheck disable=SC1091
49. /etc/os-release
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
53esac
54
55ARCH="$(dpkg --print-architecture)"
56[[ "$ARCH" == "amd64" ]] || die "Obsidian publishes a Linux .deb for amd64 only (detected: $ARCH)."
57log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
58
59export DEBIAN_FRONTEND=noninteractive
60log "Installing prerequisites..."
61run "apt-get update -qq"
62run "apt-get install -y curl jq ca-certificates"
63
64GH_HDRS=(-H "Accept: application/vnd.github+json")
65[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
66
67log "Querying GitHub API for latest release of $REPO..."
68RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
69TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
70[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
71log "Latest release: $TAG"
72
73ASSET_REGEX='^obsidian_[^/]+_amd64\.deb$'
74DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
75ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | .name' <<<"$RELEASE_JSON" | head -n1)"
76EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)"
77[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No amd64 .deb asset found in latest Obsidian release."
78
79STAGE="$(mktemp -d -t obsidian.XXXXXX)"
80trap 'rm -rf "$STAGE"' EXIT
81DEB="$STAGE/$ASSET_NAME"
82
83log "Downloading $ASSET_NAME..."
84run "curl -fsSL -o '$DEB' '$DOWNLOAD_URL'"
85
86if (( DRY_RUN )); then
87 [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \
88 && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \
89 || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n'
90 printf ' DRY-RUN: apt-get install -y %s\n' "$DEB"
91 log "Done."
92 exit 0
93fi
94
95if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then
96 log "Verifying SHA-256..."
97 ACTUAL_SHA="$(sha256sum "$DEB" | awk '{print $1}')"
98 [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
99 log "SHA-256 ok."
100else
101 warn "No SHA-256 asset digest in GitHub release metadata; skipping verification."
102fi
103
104log "Installing Obsidian..."
105run "apt-get install -y '$DEB'"
106
107if (( ! DRY_RUN )) && command -v obsidian >/dev/null 2>&1; then
108 log "Installed: $(obsidian --version 2>/dev/null || echo "$TAG")"
109fi
110log "Done."
111
install-qbittorrent.sh 原始檔案
1#!/usr/bin/env bash
2# install-qbittorrent.sh — Install qBittorrent from the latest official GitHub release.
3# Hardened: arch-aware, GitHub API token support, SHA-256 verification from the
4# release asset digest, idempotent, --dry-run.
5
6set -euo pipefail
7IFS=$'\n\t'
8
9readonly SCRIPT_NAME="${0##*/}"
10DRY_RUN=0
11REPO="qbittorrent/qBittorrent"
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Resolves the latest release of qbittorrent/qBittorrent, downloads the official
18Linux x86_64 AppImage, verifies its SHA-256 against the GitHub release asset
19digest when available, and installs it system-wide.
20
21If \$GITHUB_TOKEN is set in the environment, it is used to authenticate the
22GitHub API request (avoids rate limits).
23
24Options:
25 --dry-run Print actions without executing.
26 --help, -h Show this help.
27EOF
28}
29
30log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
31warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
32die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
33run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
34trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
35
36while (( $# )); do
37 case "$1" in
38 --dry-run) DRY_RUN=1 ;;
39 -h|--help) usage; exit 0 ;;
40 *) die "Unknown argument: $1 (try --help)" ;;
41 esac
42 shift
43done
44
45(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
46
47[[ -r /etc/os-release ]] || die "/etc/os-release not found."
48# shellcheck disable=SC1091
49. /etc/os-release
50case "${ID:-}:${ID_LIKE:-}" in
51 *ubuntu*|*debian*) : ;;
52 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
53esac
54
55ARCH="$(dpkg --print-architecture)"
56[[ "$ARCH" == "amd64" ]] || die "qBittorrent publishes official Linux AppImages for x86_64/amd64 only (detected: $ARCH)."
57log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
58
59export DEBIAN_FRONTEND=noninteractive
60log "Installing prerequisites..."
61run "apt-get update -qq"
62run "apt-get install -y curl jq ca-certificates desktop-file-utils shared-mime-info hicolor-icon-theme"
63
64GH_HDRS=(-H "Accept: application/vnd.github+json")
65[[ -n "${GITHUB_TOKEN:-}" ]] && GH_HDRS+=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
66
67log "Querying GitHub API for latest release of $REPO..."
68RELEASE_JSON="$(curl -fsSL "${GH_HDRS[@]}" "https://api.github.com/repos/${REPO}/releases/latest")"
69TAG="$(jq -r '.tag_name // empty' <<<"$RELEASE_JSON")"
70[[ -n "$TAG" ]] || die "Could not parse latest release tag (rate limited? set GITHUB_TOKEN)."
71VERSION="${TAG#release-}"
72log "Latest release: $TAG"
73
74ASSET_REGEX='^qbittorrent-[0-9][^/]*_x86_64\.AppImage$'
75DOWNLOAD_URL="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | .browser_download_url' <<<"$RELEASE_JSON" | head -n1)"
76ASSET_NAME="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | .name' <<<"$RELEASE_JSON" | head -n1)"
77EXPECTED_SHA="$(jq -r --arg re "$ASSET_REGEX" '.assets[] | select(.name | test($re)) | select(.name | test("_lt20_") | not) | (.digest // "") | sub("^sha256:"; "")' <<<"$RELEASE_JSON" | head -n1)"
78[[ "$DOWNLOAD_URL" =~ ^https:// ]] || die "No standard x86_64 AppImage asset found in latest qBittorrent release."
79
80STAGE="$(mktemp -d -t qbittorrent.XXXXXX)"
81trap 'rm -rf "$STAGE"' EXIT
82APPIMAGE="$STAGE/$ASSET_NAME"
83
84log "Downloading $ASSET_NAME..."
85run "curl -fL -o '$APPIMAGE' '$DOWNLOAD_URL'"
86
87if (( DRY_RUN )); then
88 [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]] \
89 && printf ' DRY-RUN: verify SHA-256 %s\n' "$EXPECTED_SHA" \
90 || printf ' DRY-RUN: skip SHA-256 verification; no GitHub asset digest found\n'
91 printf ' DRY-RUN: install AppImage to /opt/qbittorrent/qbittorrent\n'
92 printf ' DRY-RUN: create /usr/local/bin/qbittorrent symlink\n'
93 printf ' DRY-RUN: install desktop launcher, icons, and MIME handlers\n'
94 log "Done."
95 exit 0
96fi
97
98if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" != "null" ]]; then
99 log "Verifying SHA-256..."
100 ACTUAL_SHA="$(sha256sum "$APPIMAGE" | awk '{print $1}')"
101 [[ "$EXPECTED_SHA" == "$ACTUAL_SHA" ]] || die "SHA-256 mismatch: expected=$EXPECTED_SHA actual=$ACTUAL_SHA"
102 log "SHA-256 ok."
103else
104 warn "No SHA-256 asset digest in GitHub release metadata; skipping verification."
105fi
106
107log "Installing qBittorrent AppImage..."
108run "install -d -m 0755 /opt/qbittorrent"
109run "install -m 0755 '$APPIMAGE' /opt/qbittorrent/qbittorrent"
110run "ln -sfn /opt/qbittorrent/qbittorrent /usr/local/bin/qbittorrent"
111
112log "Extracting desktop metadata and icons..."
113EXTRACT_DIR="$STAGE/extract"
114run "install -d -m 0755 '$EXTRACT_DIR'"
115(
116 cd "$EXTRACT_DIR"
117 /opt/qbittorrent/qbittorrent --appimage-extract >/dev/null
118)
119
120run "install -d -m 0755 /usr/share/icons/hicolor/scalable/apps"
121run "install -m 0644 '$EXTRACT_DIR/squashfs-root/usr/share/icons/hicolor/scalable/apps/qbittorrent.svg' /usr/share/icons/hicolor/scalable/apps/qbittorrent.svg"
122for size in 16 22 24 32 36 48 64 72 96 128 192; do
123 src="$EXTRACT_DIR/squashfs-root/usr/share/icons/hicolor/${size}x${size}/apps/qbittorrent.png"
124 [[ -f "$src" ]] || continue
125 run "install -d -m 0755 '/usr/share/icons/hicolor/${size}x${size}/apps'"
126 run "install -m 0644 '$src' '/usr/share/icons/hicolor/${size}x${size}/apps/qbittorrent.png'"
127done
128
129log "Installing desktop launcher..."
130cat >/usr/share/applications/org.qbittorrent.qBittorrent.desktop <<EOF
131[Desktop Entry]
132Type=Application
133Name=qBittorrent
134GenericName=BitTorrent client
135Comment=Download and share files over BitTorrent
136Exec=/opt/qbittorrent/qbittorrent %U
137Icon=qbittorrent
138Terminal=false
139Categories=Network;FileTransfer;P2P;Qt;
140MimeType=application/x-bittorrent;x-scheme-handler/magnet;
141StartupNotify=false
142StartupWMClass=qbittorrent
143Keywords=bittorrent;torrent;magnet;download;p2p;
144SingleMainWindow=true
145EOF
146
147run "desktop-file-validate /usr/share/applications/org.qbittorrent.qBittorrent.desktop"
148run "update-desktop-database /usr/share/applications"
149run "update-mime-database /usr/share/mime"
150run "gtk-update-icon-cache -f -t /usr/share/icons/hicolor >/dev/null 2>&1 || true"
151
152if command -v xdg-mime >/dev/null 2>&1; then
153 run "xdg-mime default org.qbittorrent.qBittorrent.desktop application/x-bittorrent || true"
154 run "xdg-mime default org.qbittorrent.qBittorrent.desktop x-scheme-handler/magnet || true"
155fi
156
157if command -v qbittorrent >/dev/null 2>&1; then
158 log "Installed: $(qbittorrent --version 2>/dev/null || echo "$VERSION")"
159fi
160log "Done."
161
install-screenshot-cleanup-cron.sh 原始檔案
1#!/usr/bin/env bash
2# install-screenshot-cleanup-cron.sh — Clear ~/Pictures/Screenshots every 5 minutes.
3# Idempotent per-user crontab installer.
4
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9readonly BEGIN_MARKER="# BEGIN managed by install-screenshot-cleanup-cron.sh"
10readonly END_MARKER="# END managed by install-screenshot-cleanup-cron.sh"
11DRY_RUN=0
12
13usage() {
14 cat <<EOF
15Usage: $SCRIPT_NAME [--dry-run] [--help]
16
17Installs a per-user cron job that deletes everything inside:
18
19 \$HOME/Pictures/Screenshots
20
21every 5 minutes. The Screenshots directory itself is preserved.
22
23Options:
24 --dry-run Print actions without executing.
25 --help, -h Show this help.
26EOF
27}
28
29log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31
32while (( $# )); do
33 case "$1" in
34 --dry-run) DRY_RUN=1 ;;
35 -h|--help) usage; exit 0 ;;
36 *) die "Unknown argument: $1 (try --help)" ;;
37 esac
38 shift
39done
40
41(( EUID != 0 )) || die "Do not run as root. Run as your normal desktop user so the cron job uses the correct \$HOME."
42
43for tool in crontab awk mktemp mkdir; do
44 command -v "$tool" >/dev/null 2>&1 || die "Missing required tool: $tool"
45done
46
47SCREENSHOT_DIR="$HOME/Pictures/Screenshots"
48CRON_LINE="*/5 * * * * find \"$SCREENSHOT_DIR\" -mindepth 1 -delete"
49STAGE="$(mktemp -d -t screenshot-cron.XXXXXX)"
50trap 'rm -rf "$STAGE"' EXIT
51
52CURRENT="$STAGE/current"
53NEXT="$STAGE/next"
54crontab -l >"$CURRENT" 2>/dev/null || true
55
56awk -v begin="$BEGIN_MARKER" -v end="$END_MARKER" '
57 $0 == begin { skip = 1; next }
58 $0 == end { skip = 0; next }
59 skip { next }
60 $0 == "# Delete screenshot contents every 5 minutes" { next }
61 index($0, "Pictures/Screenshots") && index($0, "-mindepth 1") && index($0, "-delete") { next }
62 { print }
63' "$CURRENT" >"$NEXT"
64
65{
66 printf '%s\n' "$BEGIN_MARKER"
67 printf '# Delete screenshot contents every 5 minutes\n'
68 printf '%s\n' "$CRON_LINE"
69 printf '%s\n' "$END_MARKER"
70} >>"$NEXT"
71
72if (( DRY_RUN )); then
73 log "Would ensure directory exists: $SCREENSHOT_DIR"
74 log "Would install this user crontab:"
75 sed 's/^/ /' "$NEXT"
76 exit 0
77fi
78
79mkdir -p "$SCREENSHOT_DIR"
80crontab "$NEXT"
81
82log "Installed screenshot cleanup cron job:"
83printf ' %s\n' "$CRON_LINE"
84log "Done."
85
install-telegram.sh 原始檔案
1#!/usr/bin/env bash
2# install-telegram.sh — Install Telegram Desktop from the latest official Linux build.
3# Hardened: official download endpoint, arch-aware, idempotent, --dry-run.
4
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10DOWNLOAD_URL="https://telegram.org/dl/desktop/linux"
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
15
16Downloads Telegram Desktop from Telegram's official latest Linux endpoint,
17installs it to /opt/Telegram, creates /usr/local/bin/telegram-desktop, and
18installs a desktop launcher.
19
20Options:
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
28run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
29trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
30
31while (( $# )); do
32 case "$1" in
33 --dry-run) DRY_RUN=1 ;;
34 -h|--help) usage; exit 0 ;;
35 *) die "Unknown argument: $1 (try --help)" ;;
36 esac
37 shift
38done
39
40(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
41
42[[ -r /etc/os-release ]] || die "/etc/os-release not found."
43# shellcheck disable=SC1091
44. /etc/os-release
45case "${ID:-}:${ID_LIKE:-}" in
46 *ubuntu*|*debian*) : ;;
47 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
48esac
49
50MACHINE="$(uname -m)"
51case "$MACHINE" in
52 x86_64|amd64) : ;;
53 *) die "Telegram's official Linux desktop tarball targets x86_64 (detected: $MACHINE)." ;;
54esac
55log "Detected: ${PRETTY_NAME:-unknown}, arch: $MACHINE"
56
57export DEBIAN_FRONTEND=noninteractive
58log "Installing prerequisites..."
59run "apt-get update -qq"
60run "apt-get install -y curl ca-certificates tar xz-utils desktop-file-utils hicolor-icon-theme"
61
62STAGE="$(mktemp -d -t telegram.XXXXXX)"
63trap 'rm -rf "$STAGE"' EXIT
64ARCHIVE="$STAGE/telegram.tar.xz"
65
66log "Downloading latest Telegram Desktop..."
67run "curl -fL -o '$ARCHIVE' '$DOWNLOAD_URL'"
68
69if (( DRY_RUN )); then
70 printf ' DRY-RUN: extract %s\n' "$ARCHIVE"
71 printf ' DRY-RUN: install Telegram to /opt/Telegram\n'
72 printf ' DRY-RUN: create /usr/local/bin/telegram-desktop symlink and desktop launcher\n'
73 log "Done."
74 exit 0
75fi
76
77log "Extracting Telegram..."
78run "tar -xJf '$ARCHIVE' -C '$STAGE'"
79[[ -x "$STAGE/Telegram/Telegram" ]] || die "Unexpected archive layout: Telegram/Telegram not found."
80
81log "Installing Telegram..."
82run "rm -rf /opt/Telegram"
83run "install -d -m 0755 /opt"
84run "cp -a '$STAGE/Telegram' /opt/Telegram"
85run "ln -sfn /opt/Telegram/Telegram /usr/local/bin/telegram-desktop"
86
87run "install -d -m 0755 /usr/share/applications /usr/share/icons/hicolor/256x256/apps"
88if [[ -f /opt/Telegram/telegram.png ]]; then
89 run "install -m 0644 /opt/Telegram/telegram.png /usr/share/icons/hicolor/256x256/apps/telegram.png"
90fi
91
92cat >/usr/share/applications/telegram-desktop.desktop <<'EOF'
93[Desktop Entry]
94Type=Application
95Name=Telegram Desktop
96GenericName=Messenger
97Comment=Official Telegram desktop messaging app
98Exec=/opt/Telegram/Telegram -- %u
99Icon=telegram
100Terminal=false
101Categories=Network;InstantMessaging;Chat;
102MimeType=x-scheme-handler/tg;
103StartupNotify=false
104StartupWMClass=TelegramDesktop
105Keywords=telegram;chat;messenger;
106EOF
107
108run "desktop-file-validate /usr/share/applications/telegram-desktop.desktop"
109run "update-desktop-database /usr/share/applications"
110run "gtk-update-icon-cache -f -t /usr/share/icons/hicolor >/dev/null 2>&1 || true"
111if command -v xdg-mime >/dev/null 2>&1; then
112 run "xdg-mime default telegram-desktop.desktop x-scheme-handler/tg || true"
113fi
114
115log "Installed: $(/opt/Telegram/Telegram --version 2>/dev/null || echo Telegram Desktop)"
116log "Done."
117
install-thunderbird.sh 原始檔案
1#!/usr/bin/env bash
2# install-thunderbird.sh — Install Thunderbird.
3# On Ubuntu: uses the Mozilla Team PPA (with APT pinning so Snap transition is bypassed).
4# On Debian: uses the regular Debian package (no PPA available).
5# Hardened: distro-detect, idempotent, --dry-run.
6
7set -euo pipefail
8IFS=$'\n\t'
9
10readonly SCRIPT_NAME="${0##*/}"
11DRY_RUN=0
12
13usage() {
14 cat <<EOF
15Usage: sudo $SCRIPT_NAME [--dry-run] [--help]
16
17Installs Thunderbird.
18 - Ubuntu: removes Snap version (if any), adds Mozilla Team PPA, pins it,
19 and installs the .deb. This avoids the Ubuntu snap transition wrapper.
20 - Debian: installs from the standard Debian repos.
21
22Options:
23 --dry-run Print actions without executing.
24 --help, -h Show this help.
25EOF
26}
27
28log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
29warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
30die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
31run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
32trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
33
34while (( $# )); do
35 case "$1" in
36 --dry-run) DRY_RUN=1 ;;
37 -h|--help) usage; exit 0 ;;
38 *) die "Unknown argument: $1 (try --help)" ;;
39 esac
40 shift
41done
42
43(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
44
45[[ -r /etc/os-release ]] || die "/etc/os-release not found."
46# shellcheck disable=SC1091
47. /etc/os-release
48log "Detected: ${PRETTY_NAME:-unknown} (id=${ID:-?})"
49
50export DEBIAN_FRONTEND=noninteractive
51
52case "${ID:-}" in
53 ubuntu)
54 log "Removing Thunderbird Snap (if present)..."
55 if command -v snap >/dev/null 2>&1; then
56 run "snap remove --purge thunderbird >/dev/null 2>&1 || true"
57 fi
58
59 log "Installing prerequisites..."
60 run "apt-get update -qq"
61 run "apt-get install -y software-properties-common"
62
63 log "Adding Mozilla Team PPA..."
64 PPA_LIST=/etc/apt/sources.list.d/mozillateam-ubuntu-ppa-*.list
65 # shellcheck disable=SC2086
66 if ! ls $PPA_LIST >/dev/null 2>&1; then
67 run "add-apt-repository -y ppa:mozillateam/ppa"
68 else
69 log "Mozilla Team PPA already configured."
70 fi
71
72 PIN=/etc/apt/preferences.d/mozillateamppa
73 log "Pinning Mozilla Team PPA so .deb wins over Snap transition wrapper..."
74 if [[ ! -f "$PIN" ]] || ! grep -q 'release o=LP-PPA-mozillateam' "$PIN"; then
75 if (( DRY_RUN )); then
76 printf ' DRY-RUN: write %s\n' "$PIN"
77 else
78 cat >"$PIN" <<'EOF'
79Package: thunderbird*
80Pin: release o=LP-PPA-mozillateam
81Pin-Priority: 1001
82EOF
83 fi
84 fi
85
86 log "Installing thunderbird from PPA..."
87 run "apt-get update -qq"
88 run "apt-get install -y --allow-downgrades thunderbird"
89 ;;
90
91 debian)
92 log "Installing thunderbird from Debian repos..."
93 run "apt-get update -qq"
94 run "apt-get install -y thunderbird"
95 ;;
96
97 *)
98 die "Unsupported distro: ${PRETTY_NAME:-unknown}. Supported: ubuntu, debian."
99 ;;
100esac
101
102log "Done."
103
install-vscode.sh 原始檔案
1#!/usr/bin/env bash
2# install-vscode.sh — Install Visual Studio Code from Microsoft's APT repository.
3# Hardened: arch-aware, signed-by keyring, idempotent, --dry-run.
4
5set -euo pipefail
6IFS=$'\n\t'
7
8readonly SCRIPT_NAME="${0##*/}"
9DRY_RUN=0
10INSIDERS=0
11
12usage() {
13 cat <<EOF
14Usage: sudo $SCRIPT_NAME [--dry-run] [--insiders] [--help]
15
16Configures the official Microsoft 'vscode' APT repository (signed-by keyring)
17and installs Visual Studio Code. Idempotent: safe to re-run.
18
19Options:
20 --insiders Install the 'code-insiders' build instead of stable 'code'.
21 --dry-run Print actions without executing.
22 --help, -h Show this help.
23EOF
24}
25
26log() { printf '\033[1;34m[%s]\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*"; }
27warn() { printf '\033[1;33m[%s] WARN:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; }
28die() { printf '\033[1;31m[%s] ERROR:\033[0m %s\n' "${SCRIPT_NAME%.sh}" "$*" >&2; exit 1; }
29run() { if (( DRY_RUN )); then printf ' DRY-RUN: %s\n' "$*"; else eval "$@"; fi; }
30trap 'rc=$?; (( rc )) && printf "\033[1;31m[%s] failed at line %s (exit %d)\033[0m\n" "${SCRIPT_NAME%.sh}" "$LINENO" "$rc" >&2' ERR
31
32while (( $# )); do
33 case "$1" in
34 --dry-run) DRY_RUN=1 ;;
35 --insiders) INSIDERS=1 ;;
36 -h|--help) usage; exit 0 ;;
37 *) die "Unknown argument: $1 (try --help)" ;;
38 esac
39 shift
40done
41
42(( EUID == 0 )) || die "Must run as root. Try: sudo $SCRIPT_NAME"
43
44[[ -r /etc/os-release ]] || die "/etc/os-release not found."
45# shellcheck disable=SC1091
46. /etc/os-release
47case "${ID:-}:${ID_LIKE:-}" in
48 *ubuntu*|*debian*) : ;;
49 *) die "Unsupported distro: ${PRETTY_NAME:-unknown}." ;;
50esac
51
52ARCH="$(dpkg --print-architecture)"
53case "$ARCH" in
54 amd64|arm64|armhf) : ;;
55 *) die "VS Code is published for amd64/arm64/armhf (detected: $ARCH)." ;;
56esac
57log "Detected: ${PRETTY_NAME:-unknown}, arch: $ARCH"
58
59export DEBIAN_FRONTEND=noninteractive
60
61KEYRING=/etc/apt/keyrings/packages.microsoft.gpg
62SOURCES=/etc/apt/sources.list.d/vscode.list
63PKG=$([[ $INSIDERS -eq 1 ]] && echo code-insiders || echo code)
64
65log "Installing prerequisites..."
66run "apt-get update -qq"
67run "apt-get install -y wget gpg apt-transport-https ca-certificates"
68
69log "Configuring Microsoft vscode APT repository..."
70run "install -d -m 0755 /etc/apt/keyrings"
71if [[ ! -s "$KEYRING" ]]; then
72 run "wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o '$KEYRING'"
73 run "chmod 0644 '$KEYRING'"
74fi
75
76DESIRED_SRC="deb [arch=amd64,arm64,armhf signed-by=${KEYRING}] https://packages.microsoft.com/repos/code stable main"
77if [[ ! -f "$SOURCES" ]] || ! grep -qxF "$DESIRED_SRC" "$SOURCES"; then
78 run "printf '%s\n' '$DESIRED_SRC' > '$SOURCES'"
79fi
80
81log "Installing $PKG..."
82run "apt-get update -qq"
83run "apt-get install -y '$PKG'"
84
85if (( ! DRY_RUN )) && command -v "$PKG" >/dev/null 2>&1; then
86 log "Installed: $("$PKG" --version 2>/dev/null | head -n1 || echo "$PKG")"
87fi
88log "Done."
89
menu.sh 原始檔案
1#!/usr/bin/env bash
2# menu.sh — Ubuntu/Debian Setup Manager
3#
4# Interactive menu that fetches and runs the hardened install scripts in this
5# Opengist. Each script is downloaded to a temp file (not blindly piped to bash)
6# and executed with the appropriate privilege level for that script:
7#
8# - "sudo" mode for system-wide installers (apt, /etc, /usr/local/bin)
9# - "user" mode for per-user installers that must NOT run as root
10# (gsettings, ~/.local, JetBrains Toolbox, fonts)
11#
12# Usage:
13# bash -c "$(curl -fsSL https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh)"
14#
15# Or save and run locally:
16# curl -fsSLO https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD/menu.sh
17# bash menu.sh
18
19# Note: NOT using `set -e` because we want the menu loop to survive a failed
20# sub-script. We do use -u and pipefail to catch real bugs in this file.
21set -uo pipefail
22IFS=$'\n\t'
23
24readonly SCRIPT_NAME="${0##*/}"
25readonly BASE_URL='https://opengist.rmrf.online/weehong/2de15ba0106a475fa41215159203a63b/raw/HEAD'
26
27log() { printf '\033[1;34m[menu]\033[0m %s\n' "$*"; }
28warn() { printf '\033[1;33m[menu] WARN:\033[0m %s\n' "$*" >&2; }
29err() { printf '\033[1;31m[menu] ERROR:\033[0m %s\n' "$*" >&2; }
30hr() { printf '%s\n' "------------------------------------------------------"; }
31
32# Refuse to run as root: per-user scripts (jetbrains, fonts, ibus) need a real
33# desktop user. Sub-scripts elevate via sudo on their own.
34if (( EUID == 0 )); then
35 err "Don't run $SCRIPT_NAME as root. Run as your normal user — it will call sudo for installers that need it."
36 exit 1
37fi
38
39# Sanity-check tools we depend on.
40for tool in curl bash mktemp; do
41 command -v "$tool" >/dev/null 2>&1 || { err "Missing required tool: $tool"; exit 1; }
42done
43
44# Distro check (warn-only; sub-scripts enforce strictly).
45if [[ -r /etc/os-release ]]; then
46 # shellcheck disable=SC1091
47 . /etc/os-release
48 case "${ID:-}:${ID_LIKE:-}" in
49 *ubuntu*|*debian*) : ;;
50 *) warn "Detected ${PRETTY_NAME:-unknown}. These installers target Debian/Ubuntu; some will refuse to run." ;;
51 esac
52fi
53
54# Cache sudo credentials up-front so sub-scripts that elevate don't keep
55# re-prompting in the middle of a multi-task run.
56prime_sudo() {
57 if ! sudo -n true 2>/dev/null; then
58 log "Caching sudo credentials (you may be prompted)..."
59 sudo -v || { err "sudo authentication failed."; return 1; }
60 fi
61 # Keep sudo timestamp refreshed in the background while the menu runs.
62 ( while true; do sudo -n true 2>/dev/null; sleep 60; done ) &
63 SUDO_KEEPALIVE_PID=$!
64 trap 'kill "${SUDO_KEEPALIVE_PID:-}" 2>/dev/null || true' EXIT
65}
66
67# run_script <filename> <mode>
68# mode: "sudo" (run as root via sudo) or "user" (run as current user)
69# Returns: 0 on success, non-zero on failure (does NOT exit the menu).
70run_script() {
71 local name="$1" mode="$2"
72 local url="$BASE_URL/$name"
73 local tmp
74 tmp="$(mktemp -t "${name%.sh}.XXXXXX.sh")" || { err "mktemp failed"; return 1; }
75
76 log "Fetching $name..."
77 if ! curl -fsSL --max-time 60 --retry 2 "$url" -o "$tmp"; then
78 err "Download failed: $url"
79 rm -f "$tmp"
80 return 1
81 fi
82 if [[ ! -s "$tmp" ]]; then
83 err "Downloaded $name is empty."
84 rm -f "$tmp"
85 return 1
86 fi
87 # Cheap sanity: confirm it looks like a shell script.
88 if ! head -n1 "$tmp" | grep -qE '^#!.*sh'; then
89 warn "$name doesn't start with a shebang; proceeding anyway."
90 fi
91 chmod +x "$tmp"
92
93 local rc=0
94 if [[ "$mode" == "sudo" ]]; then
95 sudo bash "$tmp" || rc=$?
96 else
97 bash "$tmp" || rc=$?
98 fi
99 rm -f "$tmp"
100 if (( rc != 0 )); then
101 err "$name exited with status $rc"
102 fi
103 return "$rc"
104}
105
106# Catalog: number | label | script-filename | mode
107# (Edit here to add/remove options — the menu loop is data-driven.)
108OPTIONS=(
109 "1|Install Google Chrome|install-chrome.sh|sudo"
110 "2|Install Firefox (Mozilla APT)|install-firefox.sh|sudo"
111 "3|Install Thunderbird|install-thunderbird.sh|sudo"
112 "4|Install LocalSend|install-localsend.sh|sudo"
113 "5|Install Telegram Desktop|install-telegram.sh|sudo"
114 "6|Install Discord|install-discord.sh|sudo"
115 "7|Install 1Password|install-1password.sh|sudo"
116 "8|Install Espanso (text expander)|install-espanso.sh|sudo"
117 "9|Install LibreOffice (latest stable .deb)|install-libreoffice.sh|sudo"
118 "10|Install Obsidian|install-obsidian.sh|sudo"
119 "11|Install draw.io Desktop|install-drawio.sh|sudo"
120 "12|Install Visual Studio Code|install-vscode.sh|sudo"
121 "13|Install JetBrains Toolbox (per-user)|install-jetbrains-toolbox.sh|user"
122 "14|Install Bruno (API client)|install-bruno.sh|sudo"
123 "15|Install IPATool|install-ipatool.sh|sudo"
124 "16|Install qBittorrent (latest AppImage)|install-qbittorrent.sh|sudo"
125 "17|Mount Synology Network Drive|install-network_drive.sh|sudo"
126 "18|Install IBus Intelligent Pinyin (per-user)|install-ibus-pinyin.sh|user"
127 "19|Install Nerd Fonts (per-user)|install-font.sh|user"
128 "20|Fix Dual-Boot Time (RTC to UTC)|timedatectl-fix.sh|sudo"
129 "21|Install Screenshots Cleanup Cron (per-user)|install-screenshot-cleanup-cron.sh|user"
130)
131
132print_menu() {
133 hr
134 echo " Ubuntu / Debian Setup Manager"
135 hr
136 echo "--- [ Browsers & Mail ] ---"
137 echo " 1) Install Google Chrome"
138 echo " 2) Install Firefox"
139 echo " 3) Install Thunderbird"
140 echo "--- [ Communication ] ---"
141 echo " 4) Install LocalSend"
142 echo " 5) Install Telegram Desktop"
143 echo " 6) Install Discord"
144 echo "--- [ Productivity & Security ] ---"
145 echo " 7) Install 1Password"
146 echo " 8) Install Espanso"
147 echo " 9) Install LibreOffice"
148 echo " 10) Install Obsidian"
149 echo " 11) Install draw.io Desktop"
150 echo "--- [ Development Tools ] ---"
151 echo " 12) Install Visual Studio Code"
152 echo " 13) Install JetBrains Toolbox (runs as you, not root)"
153 echo " 14) Install Bruno"
154 echo " 15) Install IPATool"
155 echo " 16) Install qBittorrent"
156 echo "--- [ System ] ---"
157 echo " 17) Mount Synology Network Drive"
158 echo " 18) Install IBus Intelligent Pinyin (runs as you, not root)"
159 echo " 19) Install Nerd Fonts (runs as you, not root)"
160 echo " 20) Fix Dual-Boot Time (RTC to UTC)"
161 echo " 21) Install Screenshots Cleanup Cron (runs as you, not root)"
162 hr
163 echo " 0) Run ALL options (1-21)"
164 echo " -1) Exit"
165 hr
166}
167
168# Resolve a numeric choice to its catalog entry; print "name|mode" to stdout.
169resolve_choice() {
170 local want="$1" entry num
171 for entry in "${OPTIONS[@]}"; do
172 num="${entry%%|*}"
173 if [[ "$num" == "$want" ]]; then
174 # Strip the leading "N|label|"; what remains is "filename|mode"
175 printf '%s' "${entry#*|*|}"
176 return 0
177 fi
178 done
179 return 1
180}
181
182ALL_NUMS=(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21)
183
184prime_sudo || exit 1
185
186while true; do
187 print_menu
188 read -rp "Enter choices separated by spaces (e.g., 1 7 12), 0 for ALL, -1 to exit: " choices </dev/tty || {
189 echo; log "Input closed; exiting."
190 break
191 }
192
193 # Trim whitespace
194 choices="${choices##[[:space:]]}"
195 choices="${choices%%[[:space:]]}"
196
197 if [[ -z "$choices" ]]; then
198 continue
199 fi
200
201 if [[ "$choices" == "-1" ]]; then
202 log "Exiting."
203 break
204 fi
205
206 # Expand "0" to all options
207 if [[ "$choices" == "0" ]]; then
208 choices="${ALL_NUMS[*]}"
209 fi
210
211 # Validate every token first; reject the whole batch if any token is bogus,
212 # so the user sees the problem before any work starts.
213 bad=""
214 for c in $choices; do
215 if ! [[ "$c" =~ ^-?[0-9]+$ ]] || ! resolve_choice "$c" >/dev/null; then
216 bad+=" $c"
217 fi
218 done
219 if [[ -n "$bad" ]]; then
220 err "Invalid option(s):$bad"
221 sleep 1
222 continue
223 fi
224
225 failures=()
226 for c in $choices; do
227 spec="$(resolve_choice "$c")"
228 name="${spec%|*}"
229 mode="${spec##*|}"
230 hr
231 log "[$c] Running $name ($mode)..."
232 if ! run_script "$name" "$mode"; then
233 failures+=("$c:$name")
234 fi
235 done
236
237 hr
238 if (( ${#failures[@]} == 0 )); then
239 log "All selected tasks completed."
240 else
241 warn "Completed with ${#failures[@]} failure(s): ${failures[*]}"
242 fi
243 echo
244done
245
timedatectl-fix.sh 原始檔案
1#!/usr/bin/env bash
2
3set -euo pipefail
4
5echo "======================================"
6echo " Ubuntu Dual-Boot Time Fix Script"
7echo " (UTC Method - Highly Recommended)"
8echo "======================================"
9echo
10
11# Ensure running as root
12if [[ $EUID -ne 0 ]]; then
13 echo "Please run as root:"
14 echo "sudo bash fix-time.sh"
15 exit 1
16fi
17
18# Detect current timezone instead of hardcoding
19CURRENT_TZ=$(timedatectl show -p Timezone --value || echo "UTC")
20
21echo "[1/5] Ensuring timezone is set to $CURRENT_TZ..."
22timedatectl set-timezone "$CURRENT_TZ"
23
24echo "[2/5] Configuring RTC to use UTC (to prevent dual-boot conflicts)..."
25# '0' sets the hardware clock to UTC
26timedatectl set-local-rtc 0 --adjust-system-clock
27
28echo "[3/5] Enabling NTP synchronization..."
29timedatectl set-ntp true
30
31echo "[4/5] Restarting time sync daemon (if applicable)..."
32systemctl restart systemd-timesyncd 2>/dev/null || true
33
34echo "[5/5] Final time configuration:"
35echo
36timedatectl
37
38echo
39echo "======================================"
40echo " SUCCESS - LINUX CONFIGURED"
41echo "======================================"
42echo
43echo "Your system is now configured with:"
44echo " - Timezone: $CURRENT_TZ"
45echo " - RTC stored in UTC"
46echo " - NTP synchronization enabled"
47echo
48
49cat <<'EOF'
50--------------------------------------------------
51WINDOWS CONFIGURATION (REQUIRED)
52--------------------------------------------------
53Linux is now correctly expecting the hardware clock to be in UTC.
54To make Windows do the same and stop the time from resetting,
55you MUST run the following command in Windows.
56
571. Boot into Windows.
582. Open Command Prompt as Administrator.
593. Run this exact command:
60
61reg add "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation" /v RealTimeIsUniversal /d 1 /t REG_DWORD /f
62
634. Restart Windows and sync the time in Settings one last time.
64EOF