mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
Split three large linux_configuration scripts, defer a fourth
organize_downloads.sh 513 -> 72, setup_kcd2_dice_solver.sh 497 -> 112, fix_ubuntu_performance.sh 494 -> 150. First 490+ line scripts done; all three needed two or three seams. Repeated splitting chained the libs - each sourced the next, because the tool inserts its source line relative to the file it just wrote. Flattened so the entry sources all three and no lib sources a sibling. diagnose_pacman_hook_stall.sh is reverted and stays at 493. Its transaction runner assigns LAST_ELAPSED that the caller reads and reads PACMAN_BIN that the caller defines, so linted alone it is SC2034 plus SC2153. The pre-commit hook lints each lib in isolation and fails, while shellcheck -x on the entry passes - suppressing that would be papering over a seam that does not exist. organize_downloads.sh is the ExecStart of media-organizer.service; the unit still resolves and the script still parses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
parent
4ac8c77abd
commit
2527fb37e6
@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "Split three large linux_configuration scripts, deferring a fourth",
|
||||
"objective": "Bring organize_downloads.sh (513), setup_kcd2_dice_solver.sh (497) and fix_ubuntu_performance.sh (494) under the 250-line cap, and establish that a seam sharing mutable state with its caller must be abandoned rather than suppressed.",
|
||||
"acceptance_criteria": [
|
||||
"All three entry scripts and all seven new libs are at most 250 lines.",
|
||||
"The pre-commit shellcheck hook passes on every touched file, with no new suppression and no disable directive. That hook lints each lib in isolation, which is stricter than shellcheck -x on the entry script.",
|
||||
"No lib sources a sibling: the entry script sources every lib directly.",
|
||||
"The stubbed run reports every moved function as defined, for all three scripts.",
|
||||
"organize_downloads.sh is the ExecStart of the live media-organizer.service unit, so after the split that path must still resolve and the script must still parse.",
|
||||
"No script is executed. These move files in ~/Downloads, write compose stacks and change sysctl and systemd settings.",
|
||||
"diagnose_pacman_hook_stall.sh is fully restored to 493 lines, leaving no partial split behind."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Executing any organize, setup or fix step.",
|
||||
"diagnose_pacman_hook_stall.sh, whose load generator assigns LAST_ELAPSED read by the caller and reads PACMAN_BIN defined by it; splitting it would need a suppression.",
|
||||
"Changing what any script does when run."
|
||||
],
|
||||
"verifier": "meta/scripts/verify_shell_split.sh per script, plus pre-commit run shellcheck over all touched files, plus systemctl cat media-organizer and test -f on its ExecStart, plus check_file_length.sh and the real git commit for jscpd."
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
{
|
||||
"intent": "Split three of the largest remaining linux_configuration scripts, and defer a fourth whose seam cannot be made lint-clean without a suppression.",
|
||||
"scope": [
|
||||
"single_use/utils/organize_downloads.sh (513 -> 72) + lib/download_classify.sh, download_sizes.sh, download_media.sh",
|
||||
"single_use/features/setup_kcd2_dice_solver.sh (497 -> 112) + lib/kcd2_serve.sh, kcd2_expose.sh, kcd2_autorebuild.sh",
|
||||
"single_use/fixes/fix_ubuntu_performance.sh (494 -> 150) + lib/ubuntu_perf_fixes.sh, ubuntu_perf_more.sh",
|
||||
"Deferred and fully restored: single_use/fixes/diagnose_pacman_hook_stall.sh (493)",
|
||||
"Non-goal: no script executed"
|
||||
],
|
||||
"changes": [
|
||||
"Three cuts each for organize_downloads.sh and setup_kcd2_dice_solver.sh, two for fix_ubuntu_performance.sh. These are the first 490+ line scripts done, and every one needed more than one seam.",
|
||||
"Repeated splitting chained the libs - each new lib sourced the next, because the tool inserts its source line relative to the file it just wrote. Flattened so organize_downloads.sh sources all three directly and no lib sources a sibling, and removed a leftover SCRIPT_DIR the chain had left in a lib.",
|
||||
"diagnose_pacman_hook_stall.sh was reverted to HEAD. Its transaction runner assigns LAST_ELAPSED which the caller reads, and reads PACMAN_BIN which the caller defines; linted as a fragment that is SC2034 plus SC2153, and the pre-commit hook lints each lib in isolation. Neither is a real bug, which is exactly why suppressing them would be wrong - the honest answer is that this seam does not exist."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "meta/scripts/verify_shell_split.sh per script",
|
||||
"result": "fail then pass",
|
||||
"evidence": "organize_downloads.sh first reported SC1091 for a lib sourcing a sibling; after flattening, all three report 'libs sourced, functions defined'."
|
||||
},
|
||||
{
|
||||
"command": "pre-commit run shellcheck --files <all touched>",
|
||||
"result": "fail then pass",
|
||||
"evidence": "Failed on the pacman lib's SC2034/SC2153. This is the check that matters here and is stricter than shellcheck -x on the entry script, which passed on the same code - the hook lints each lib alone, with no view of the caller's globals. Passes now that the split is reverted."
|
||||
},
|
||||
{
|
||||
"command": "systemctl cat media-organizer.service, test -f its ExecStart, bash -n that file",
|
||||
"result": "pass",
|
||||
"evidence": "Resolves to organize_downloads.sh and parses. This is one of only two units pointing into the repo, and it is the script that shrank most in this batch (513 -> 72)."
|
||||
},
|
||||
{
|
||||
"command": "git checkout HEAD -- diagnose_pacman_hook_stall.sh, then shellcheck -x",
|
||||
"result": "pass",
|
||||
"evidence": "Restored to 493 lines and clean. Note the first `git checkout` without HEAD restored the staged split rather than the original - checked with wc -l, not assumed."
|
||||
},
|
||||
{
|
||||
"command": "bash ~/utils/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "None of the three entry scripts or seven libs is listed."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"None of the three was run. organize_downloads.sh moves files in ~/Downloads and runs from a live systemd timer; verifying it by running would be exactly the mutation decision 6 forbids. The unit-path check and bash -n are the strongest available evidence.",
|
||||
"The lib-chaining bug now has two instances (20-dump-stock.sh, organize_downloads.sh). Any script split more than twice needs its source lines grepped afterwards; the tool does not do this itself.",
|
||||
"diagnose_pacman_hook_stall.sh remains at 493 lines, the largest deferred file. Splitting it needs the LAST_ELAPSED/PACMAN_BIN state passed explicitly rather than shared."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert the commit; the three scripts return to their previous sizes and the seven libs disappear.",
|
||||
"After rollback, re-run bash -n and shellcheck on the restored scripts."
|
||||
]
|
||||
}
|
||||
223
linux_configuration/scripts/single_use/features/lib/kcd2_autorebuild.sh
Executable file
223
linux_configuration/scripts/single_use/features/lib/kcd2_autorebuild.sh
Executable file
@ -0,0 +1,223 @@
|
||||
#!/bin/bash
|
||||
# Auto-rebuild timer and unit installation for the dice solver site.
|
||||
#
|
||||
# Sourced by setup_kcd2_dice_solver.sh; split out to keep kcd2_expose.sh
|
||||
# under the 250-line cap. Sourced rather than run, so it inherits the
|
||||
# caller's strict mode and the variables above the source line.
|
||||
|
||||
# --- Phase 7: auto-rebuild on commit ----------------------------------------
|
||||
# The build runs in a memory-capped systemd user unit rather than inside the
|
||||
# git hook. MemorySwapMax=0 is required, not cosmetic: this box has ~4 GB of
|
||||
# zram (swap held IN ram), so a memory-limited cgroup without it thrashes zram
|
||||
# instead of dying cleanly, and freezes the machine. A git hook running tsc and
|
||||
# vite is precisely the scenario that costs.
|
||||
install_autorebuild() {
|
||||
ensure_dir "$UNIT_DIR"
|
||||
cat >"$UNIT_FILE" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
[Unit]
|
||||
Description=Rebuild the KCD2 dice solver static bundle
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=${DICE_SRC}
|
||||
ExecStart=${DICE_BUILD}
|
||||
MemoryMax=3G
|
||||
MemorySwapMax=0
|
||||
EOF
|
||||
systemctl --user daemon-reload
|
||||
log_ok "Installed ${UNIT_FILE}."
|
||||
|
||||
[[ -d "${REPO_ROOT}/.git" ]] || die "${REPO_ROOT} is not a git repository."
|
||||
cat >"$HOOK_FILE" <<EOF
|
||||
#!/bin/bash
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
# Republishes https://${DICE_DOMAIN}/ when main gains solver changes.
|
||||
[[ "\$(git rev-parse --abbrev-ref HEAD)" == main ]] || exit 0
|
||||
git diff --name-only HEAD~1 HEAD -- kcd2_dice_solver/ | grep -q . || exit 0
|
||||
# --no-block so the commit is never slowed, || true so systemd can never fail it.
|
||||
systemctl --user start --no-block ${UNIT_NAME} || true
|
||||
EOF
|
||||
chmod +x "$HOOK_FILE"
|
||||
log_ok "Installed ${HOOK_FILE}."
|
||||
}
|
||||
|
||||
# --- Phase 8: firewall ------------------------------------------------------
|
||||
# setup_wireguard_ssh.sh owns /etc/nftables.conf and regenerates it from
|
||||
# scratch — `flush ruleset` and all — on every run. That is fine on a quiet
|
||||
# machine and rude on a busy one: it momentarily drops the whole ruleset while
|
||||
# games, VPN links and long-lived connections are relying on it. Since the only
|
||||
# thing this deployment needs is 80/443 already being accepted, check first and
|
||||
# only pay for the regeneration when the rule is genuinely missing.
|
||||
# Exact token membership rather than a regex over the rule text: a loose
|
||||
# pattern would also match "8080, 4433" and wrongly conclude the ports are open.
|
||||
web_ports_open() {
|
||||
local line ports
|
||||
while read -r line; do
|
||||
# Strip everything but digits and commas, then test for whole tokens.
|
||||
ports=",${line//[^0-9,]/},"
|
||||
if [[ $ports == *,80,* && $ports == *,443,* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < <(sudo -n nft list ruleset 2>/dev/null | grep -oE 'tcp dport \{[^}]*\}')
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_firewall() {
|
||||
if web_ports_open; then
|
||||
log_ok "Ports 80/443 already accepted — leaving the ruleset untouched."
|
||||
return 0
|
||||
fi
|
||||
log_info "Opening ports 80/443 via setup_wireguard_ssh.sh…"
|
||||
sudo bash "$WG_SCRIPT" allow-web
|
||||
}
|
||||
|
||||
# --- Phase 9: report --------------------------------------------------------
|
||||
print_report() {
|
||||
cat <<EOF
|
||||
|
||||
============================================================================
|
||||
KCD2 dice solver deployed.
|
||||
============================================================================
|
||||
Solver : https://${DICE_DOMAIN}/
|
||||
|
||||
Auto-rebuild: committing to main with changes under kcd2_dice_solver/ starts
|
||||
${UNIT_NAME}, which rebuilds dist/ in place.
|
||||
Watch it: journalctl --user -u ${UNIT_NAME} -f
|
||||
Force it: systemctl --user start ${UNIT_NAME}
|
||||
|
||||
NOTE: the rebuild builds the WORKING TREE, not the commit. An unrelated
|
||||
uncommitted edit that happens to be present when it runs will ship to the live
|
||||
site. Commit or set aside unrelated work before triggering a rebuild.
|
||||
|
||||
Acceptance test (do this on your phone):
|
||||
Turn Wi-Fi OFF (use cellular) and open https://${DICE_DOMAIN}/ —
|
||||
all 43 dice should be on one screen and tappable.
|
||||
============================================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
setup_cmd() {
|
||||
print_setup_header "KCD2 dice solver setup"
|
||||
preflight
|
||||
build_solver
|
||||
write_serve_stack
|
||||
# The snippet goes into the SHARED sites/ directory, so it is written only
|
||||
# after the backend is up and verified. Writing it first means a failure in
|
||||
# start_site leaves an orphan behind that the next `caddy reload` from any
|
||||
# other service picks up — the edge then serves this domain as a 502 and
|
||||
# starts ACME retries for a host with no backend.
|
||||
start_site
|
||||
write_snippet
|
||||
reload_caddy
|
||||
install_autorebuild
|
||||
ensure_firewall
|
||||
print_report
|
||||
log_ok "KCD2 dice solver setup complete."
|
||||
}
|
||||
|
||||
# --- status: self-diagnose --------------------------------------------------
|
||||
status_line() {
|
||||
if [[ $1 -eq 0 ]]; then
|
||||
log_ok "$2"
|
||||
else
|
||||
log_warn "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http() {
|
||||
# Prints 0 to stdout if the URL returns 200 with a non-empty body, else 1.
|
||||
# The body-size check matters: a host-matcher misconfiguration answers 200
|
||||
# with nothing in it, which a status-only check would call healthy.
|
||||
#
|
||||
# Certificates are NOT skipped for https URLs — `curl -k` would report an
|
||||
# expired or failed-ACME cert as a healthy site, which is precisely the kind
|
||||
# of green-while-broken this command exists to catch. The loopback check runs
|
||||
# over plain http, so it needs no exception.
|
||||
local url="$1" out code size
|
||||
out="$(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 10 \
|
||||
${2:+-H "Host: $2"} "$url" 2>/dev/null || echo "000 0")"
|
||||
read -r code size <<<"$out"
|
||||
[[ $code == "200" && ${size:-0} -gt 0 ]] && echo 0 || echo 1
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
print_setup_header "KCD2 dice solver status"
|
||||
|
||||
if has_cmd node; then
|
||||
status_line 0 "node present"
|
||||
else
|
||||
status_line 1 "node missing"
|
||||
fi
|
||||
if has_cmd pnpm; then
|
||||
status_line 0 "pnpm present"
|
||||
else
|
||||
status_line 1 "pnpm missing"
|
||||
fi
|
||||
if [[ -f "${DICE_SRC}/dist/index.html" ]]; then
|
||||
status_line 0 "build present (dist/index.html)"
|
||||
else
|
||||
status_line 1 "build missing — run setup"
|
||||
fi
|
||||
if docker ps --format '{{.Names}}' | grep -qx "$DICE_CONTAINER"; then
|
||||
status_line 0 "${DICE_CONTAINER} container running"
|
||||
local pid listeners
|
||||
pid="$(docker inspect -f '{{.State.Pid}}' "$DICE_CONTAINER" 2>/dev/null || echo "")"
|
||||
listeners="$(sudo -n ss -ltnp 2>/dev/null | grep "pid=${pid}," || true)"
|
||||
if grep -qE ':(80|443)\b' <<<"$listeners"; then
|
||||
status_line 1 "${DICE_CONTAINER} is CONTENDING for :80/:443 with ${CADDY_CONTAINER}"
|
||||
else
|
||||
status_line 0 "${DICE_CONTAINER} listens only on ${DICE_PORT}"
|
||||
fi
|
||||
else
|
||||
status_line 1 "${DICE_CONTAINER} container not running"
|
||||
fi
|
||||
|
||||
status_line "$(check_http "http://127.0.0.1:${DICE_PORT}/" "$DICE_DOMAIN")" \
|
||||
"local static server (127.0.0.1:${DICE_PORT}, Host: ${DICE_DOMAIN})"
|
||||
|
||||
if [[ -f $DICE_SNIPPET ]]; then
|
||||
status_line 0 "Caddy snippet present"
|
||||
else
|
||||
status_line 1 "Caddy snippet missing"
|
||||
fi
|
||||
if getent hosts "$DICE_DOMAIN" >/dev/null; then
|
||||
status_line 0 "${DICE_DOMAIN} resolves"
|
||||
else
|
||||
status_line 1 "${DICE_DOMAIN} does not resolve"
|
||||
fi
|
||||
status_line "$(check_http "https://${DICE_DOMAIN}/")" \
|
||||
"external https://${DICE_DOMAIN}/"
|
||||
|
||||
# The hook lives in .git/hooks, which is not version-controlled, so it is
|
||||
# lost on a fresh clone and must be checked rather than assumed. Spelled as
|
||||
# an explicit if: with two conditions, A && B || C really would misfire.
|
||||
if [[ -x $HOOK_FILE ]] && grep -q "$UNIT_NAME" "$HOOK_FILE" 2>/dev/null; then
|
||||
status_line 0 "post-commit auto-rebuild hook installed"
|
||||
else
|
||||
status_line 1 "post-commit hook missing — re-run setup"
|
||||
fi
|
||||
|
||||
if [[ -f $UNIT_FILE ]]; then
|
||||
status_line 0 "${UNIT_NAME} installed"
|
||||
else
|
||||
status_line 1 "${UNIT_NAME} missing — re-run setup"
|
||||
fi
|
||||
if systemctl --user is-failed --quiet "$UNIT_NAME"; then
|
||||
status_line 1 "last auto-rebuild FAILED — journalctl --user -u ${UNIT_NAME}"
|
||||
else
|
||||
status_line 0 "no failed auto-rebuild recorded"
|
||||
fi
|
||||
|
||||
# "not failed" also covers "never ran", so check the artefact rather than the
|
||||
# unit: if any source file is newer than the published bundle, the hook is
|
||||
# not firing and the site is quietly serving an old build.
|
||||
local newest_src
|
||||
newest_src="$(find "${DICE_SRC}/src" "${DICE_SRC}/index.html" -type f -newer \
|
||||
"${DICE_SRC}/dist/index.html" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n $newest_src ]]; then
|
||||
status_line 1 "published bundle is STALE (e.g. ${newest_src#"${DICE_SRC}/"}) — run: systemctl --user start ${UNIT_NAME}"
|
||||
else
|
||||
status_line 0 "published bundle is up to date with src/"
|
||||
fi
|
||||
}
|
||||
60
linux_configuration/scripts/single_use/features/lib/kcd2_expose.sh
Executable file
60
linux_configuration/scripts/single_use/features/lib/kcd2_expose.sh
Executable file
@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# Edge-conflict checks, site startup and Caddy reload.
|
||||
#
|
||||
# Sourced by setup_kcd2_dice_solver.sh; split out to keep kcd2_serve.sh
|
||||
# under the 250-line cap. Sourced rather than run, so it inherits the
|
||||
# caller's strict mode and the variables above the source line.
|
||||
|
||||
# This container shares the host network namespace with the public edge, so a
|
||||
# misconfiguration here does not fail locally — it quietly steals a share of
|
||||
# :80 or :443 from every other site on the box. Gate on it rather than trust
|
||||
# the config to stay correct.
|
||||
assert_no_edge_conflict() {
|
||||
local pid listeners
|
||||
pid="$(docker inspect -f '{{.State.Pid}}' "$DICE_CONTAINER" 2>/dev/null || echo "")"
|
||||
[[ -n $pid && $pid != "0" ]] || die "Could not read the ${DICE_CONTAINER} PID."
|
||||
|
||||
listeners="$(sudo -n ss -ltnp 2>/dev/null | grep "pid=${pid}," || true)"
|
||||
|
||||
# Demand positive evidence FIRST. Asserting only the absence of :80/:443
|
||||
# fails open: if `ss` returns nothing — a stale PID after a restart, or
|
||||
# sudo -n unavailable — every "is it bad?" test misses and the guard happily
|
||||
# reports success without having looked at anything.
|
||||
if ! grep -qE '(^|[[:space:]])127\.0\.0\.1:'"${DICE_PORT}"'([[:space:]]|$)' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "Could not confirm ${DICE_CONTAINER} listens on 127.0.0.1:${DICE_PORT}. Container stopped."
|
||||
fi
|
||||
if grep -qE ':(80|443)\b' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "${DICE_CONTAINER} bound :80/:443 — it would contend with ${CADDY_CONTAINER}. Container stopped."
|
||||
fi
|
||||
# Must be on loopback, not 0.0.0.0/*: host networking would otherwise expose
|
||||
# the unfronted static server to the whole LAN.
|
||||
if grep -qE '(\*|0\.0\.0\.0):'"${DICE_PORT}"'\b' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "${DICE_CONTAINER} bound ${DICE_PORT} on all interfaces, not loopback. Container stopped."
|
||||
fi
|
||||
log_ok "${DICE_CONTAINER} listens on loopback:${DICE_PORT} only — no contention, not LAN-exposed."
|
||||
}
|
||||
|
||||
start_site() {
|
||||
log_info "Starting the ${DICE_CONTAINER} container…"
|
||||
docker compose -f "$DICE_COMPOSE" up -d
|
||||
if wait_for_site; then
|
||||
log_ok "Solver answering on http://127.0.0.1:${DICE_PORT}/."
|
||||
else
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "Solver did not become reachable on 127.0.0.1:${DICE_PORT} (container stopped)."
|
||||
fi
|
||||
assert_no_edge_conflict
|
||||
}
|
||||
|
||||
# --- Phase 6: reload the edge -----------------------------------------------
|
||||
reload_caddy() {
|
||||
if docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1; then
|
||||
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile
|
||||
log_ok "Reloaded ${CADDY_CONTAINER}."
|
||||
else
|
||||
die "Caddy config invalid — aborting reload. Check ${SITES_DIR}/*.caddy."
|
||||
fi
|
||||
}
|
||||
103
linux_configuration/scripts/single_use/features/lib/kcd2_serve.sh
Executable file
103
linux_configuration/scripts/single_use/features/lib/kcd2_serve.sh
Executable file
@ -0,0 +1,103 @@
|
||||
#!/bin/bash
|
||||
# Compose stack, Caddy snippet and readiness polling for the dice solver.
|
||||
#
|
||||
# Sourced by setup_kcd2_dice_solver.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# --- Phase 3: the static-serve stack ----------------------------------------
|
||||
write_serve_stack() {
|
||||
ensure_dir "$DICE_DATA_DIR"
|
||||
cat >"$DICE_COMPOSE" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
services:
|
||||
${DICE_CONTAINER}:
|
||||
image: caddy:2.8
|
||||
container_name: ${DICE_CONTAINER}
|
||||
restart: unless-stopped
|
||||
# host networking is REQUIRED, not a style choice: the nftables forward
|
||||
# chain is policy drop with no accept rules, so a bridge-networked
|
||||
# container has no outbound egress at all.
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ${DICE_SRC}/dist:/srv:ro
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
EOF
|
||||
|
||||
cat >"$DICE_INNER_CADDY" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
# Static file server on loopback; fronted by ${CADDY_CONTAINER} on the public
|
||||
# domain.
|
||||
#
|
||||
# admin off is REQUIRED: this container shares the host network namespace with
|
||||
# ${CADDY_CONTAINER}, so without it both would listen on the admin API (:2019)
|
||||
# and a 'caddy reload' aimed at the edge could be applied here instead — which
|
||||
# would make this process grab 80/443 and run its own ACME.
|
||||
#
|
||||
# auto_https off and the explicit http:// scheme are EQUALLY required, and this
|
||||
# was found the hard way. A site address of "127.0.0.1:${DICE_PORT}" reads as a
|
||||
# HOSTNAME, so Caddy turns on automatic HTTPS for it: it issues a local
|
||||
# certificate, serves TLS on ${DICE_PORT} (so the plain-HTTP health check fails)
|
||||
# and — the real damage — starts an HTTP->HTTPS redirect listener on :80. Under
|
||||
# host networking that binds *:80 next to the edge, and SO_REUSEPORT then splits
|
||||
# inbound HTTP between the two, which would intermittently break ACME
|
||||
# challenges for every domain on this host.
|
||||
#
|
||||
# The site address is port-only and the loopback restriction is done with
|
||||
# "bind". These are two different things and using the wrong one breaks in a way
|
||||
# that looks like it works:
|
||||
#
|
||||
# A site address of "http://127.0.0.1:${DICE_PORT}" sets a host MATCHER of
|
||||
# 127.0.0.1. ${CADDY_CONTAINER} forwards the original "Host:
|
||||
# ${DICE_DOMAIN}" header, which then matches no route, and Caddy answers 200
|
||||
# with an empty body. A local health check passes (curl sends Host:
|
||||
# 127.0.0.1) while every real request serves a blank page.
|
||||
#
|
||||
# ":${DICE_PORT}" matches any Host, and "bind 127.0.0.1" restricts the
|
||||
# listener itself — which the address never did. Without bind, host
|
||||
# networking would expose this unfronted server to the whole LAN.
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:${DICE_PORT} {
|
||||
bind 127.0.0.1
|
||||
root * /srv
|
||||
# Single-page bundle: unknown paths should serve the app, not 404.
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
encode gzip
|
||||
}
|
||||
EOF
|
||||
log_ok "Wrote ${DICE_COMPOSE} and ${DICE_INNER_CADDY}."
|
||||
}
|
||||
|
||||
# --- Phase 4: front it on the public domain ---------------------------------
|
||||
write_snippet() {
|
||||
cat >"$DICE_SNIPPET" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
${DICE_DOMAIN} {
|
||||
reverse_proxy 127.0.0.1:${DICE_PORT}
|
||||
}
|
||||
EOF
|
||||
log_ok "Wrote ${DICE_SNIPPET}."
|
||||
}
|
||||
|
||||
# --- Phase 5: start ---------------------------------------------------------
|
||||
# Sends the Host header the edge will actually forward, and requires a
|
||||
# non-empty body. A bare "curl http://127.0.0.1:PORT/" sends Host: 127.0.0.1
|
||||
# and returns an empty 200 against a host-matcher misconfiguration — it passes
|
||||
# while every real request serves a blank page. Ask the question the edge asks.
|
||||
wait_for_site() {
|
||||
local _ size
|
||||
for _ in $(seq 1 30); do
|
||||
size="$(curl -s -o /dev/null -w '%{size_download}' \
|
||||
-H "Host: ${DICE_DOMAIN}" "http://127.0.0.1:${DICE_PORT}/" 2>/dev/null || echo 0)"
|
||||
if [[ ${size:-0} -gt 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
@ -107,373 +107,12 @@ build_solver() {
|
||||
log_ok "Solver built to ${DICE_SRC}/dist."
|
||||
}
|
||||
|
||||
# --- Phase 3: the static-serve stack ----------------------------------------
|
||||
write_serve_stack() {
|
||||
ensure_dir "$DICE_DATA_DIR"
|
||||
cat >"$DICE_COMPOSE" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
services:
|
||||
${DICE_CONTAINER}:
|
||||
image: caddy:2.8
|
||||
container_name: ${DICE_CONTAINER}
|
||||
restart: unless-stopped
|
||||
# host networking is REQUIRED, not a style choice: the nftables forward
|
||||
# chain is policy drop with no accept rules, so a bridge-networked
|
||||
# container has no outbound egress at all.
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ${DICE_SRC}/dist:/srv:ro
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
EOF
|
||||
|
||||
cat >"$DICE_INNER_CADDY" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
# Static file server on loopback; fronted by ${CADDY_CONTAINER} on the public
|
||||
# domain.
|
||||
#
|
||||
# admin off is REQUIRED: this container shares the host network namespace with
|
||||
# ${CADDY_CONTAINER}, so without it both would listen on the admin API (:2019)
|
||||
# and a 'caddy reload' aimed at the edge could be applied here instead — which
|
||||
# would make this process grab 80/443 and run its own ACME.
|
||||
#
|
||||
# auto_https off and the explicit http:// scheme are EQUALLY required, and this
|
||||
# was found the hard way. A site address of "127.0.0.1:${DICE_PORT}" reads as a
|
||||
# HOSTNAME, so Caddy turns on automatic HTTPS for it: it issues a local
|
||||
# certificate, serves TLS on ${DICE_PORT} (so the plain-HTTP health check fails)
|
||||
# and — the real damage — starts an HTTP->HTTPS redirect listener on :80. Under
|
||||
# host networking that binds *:80 next to the edge, and SO_REUSEPORT then splits
|
||||
# inbound HTTP between the two, which would intermittently break ACME
|
||||
# challenges for every domain on this host.
|
||||
#
|
||||
# The site address is port-only and the loopback restriction is done with
|
||||
# "bind". These are two different things and using the wrong one breaks in a way
|
||||
# that looks like it works:
|
||||
#
|
||||
# A site address of "http://127.0.0.1:${DICE_PORT}" sets a host MATCHER of
|
||||
# 127.0.0.1. ${CADDY_CONTAINER} forwards the original "Host:
|
||||
# ${DICE_DOMAIN}" header, which then matches no route, and Caddy answers 200
|
||||
# with an empty body. A local health check passes (curl sends Host:
|
||||
# 127.0.0.1) while every real request serves a blank page.
|
||||
#
|
||||
# ":${DICE_PORT}" matches any Host, and "bind 127.0.0.1" restricts the
|
||||
# listener itself — which the address never did. Without bind, host
|
||||
# networking would expose this unfronted server to the whole LAN.
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
:${DICE_PORT} {
|
||||
bind 127.0.0.1
|
||||
root * /srv
|
||||
# Single-page bundle: unknown paths should serve the app, not 404.
|
||||
try_files {path} /index.html
|
||||
file_server
|
||||
encode gzip
|
||||
}
|
||||
EOF
|
||||
log_ok "Wrote ${DICE_COMPOSE} and ${DICE_INNER_CADDY}."
|
||||
}
|
||||
|
||||
# --- Phase 4: front it on the public domain ---------------------------------
|
||||
write_snippet() {
|
||||
cat >"$DICE_SNIPPET" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
${DICE_DOMAIN} {
|
||||
reverse_proxy 127.0.0.1:${DICE_PORT}
|
||||
}
|
||||
EOF
|
||||
log_ok "Wrote ${DICE_SNIPPET}."
|
||||
}
|
||||
|
||||
# --- Phase 5: start ---------------------------------------------------------
|
||||
# Sends the Host header the edge will actually forward, and requires a
|
||||
# non-empty body. A bare "curl http://127.0.0.1:PORT/" sends Host: 127.0.0.1
|
||||
# and returns an empty 200 against a host-matcher misconfiguration — it passes
|
||||
# while every real request serves a blank page. Ask the question the edge asks.
|
||||
wait_for_site() {
|
||||
local _ size
|
||||
for _ in $(seq 1 30); do
|
||||
size="$(curl -s -o /dev/null -w '%{size_download}' \
|
||||
-H "Host: ${DICE_DOMAIN}" "http://127.0.0.1:${DICE_PORT}/" 2>/dev/null || echo 0)"
|
||||
if [[ ${size:-0} -gt 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# This container shares the host network namespace with the public edge, so a
|
||||
# misconfiguration here does not fail locally — it quietly steals a share of
|
||||
# :80 or :443 from every other site on the box. Gate on it rather than trust
|
||||
# the config to stay correct.
|
||||
assert_no_edge_conflict() {
|
||||
local pid listeners
|
||||
pid="$(docker inspect -f '{{.State.Pid}}' "$DICE_CONTAINER" 2>/dev/null || echo "")"
|
||||
[[ -n $pid && $pid != "0" ]] || die "Could not read the ${DICE_CONTAINER} PID."
|
||||
|
||||
listeners="$(sudo -n ss -ltnp 2>/dev/null | grep "pid=${pid}," || true)"
|
||||
|
||||
# Demand positive evidence FIRST. Asserting only the absence of :80/:443
|
||||
# fails open: if `ss` returns nothing — a stale PID after a restart, or
|
||||
# sudo -n unavailable — every "is it bad?" test misses and the guard happily
|
||||
# reports success without having looked at anything.
|
||||
if ! grep -qE '(^|[[:space:]])127\.0\.0\.1:'"${DICE_PORT}"'([[:space:]]|$)' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "Could not confirm ${DICE_CONTAINER} listens on 127.0.0.1:${DICE_PORT}. Container stopped."
|
||||
fi
|
||||
if grep -qE ':(80|443)\b' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "${DICE_CONTAINER} bound :80/:443 — it would contend with ${CADDY_CONTAINER}. Container stopped."
|
||||
fi
|
||||
# Must be on loopback, not 0.0.0.0/*: host networking would otherwise expose
|
||||
# the unfronted static server to the whole LAN.
|
||||
if grep -qE '(\*|0\.0\.0\.0):'"${DICE_PORT}"'\b' <<<"$listeners"; then
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "${DICE_CONTAINER} bound ${DICE_PORT} on all interfaces, not loopback. Container stopped."
|
||||
fi
|
||||
log_ok "${DICE_CONTAINER} listens on loopback:${DICE_PORT} only — no contention, not LAN-exposed."
|
||||
}
|
||||
|
||||
start_site() {
|
||||
log_info "Starting the ${DICE_CONTAINER} container…"
|
||||
docker compose -f "$DICE_COMPOSE" up -d
|
||||
if wait_for_site; then
|
||||
log_ok "Solver answering on http://127.0.0.1:${DICE_PORT}/."
|
||||
else
|
||||
docker stop "$DICE_CONTAINER" >/dev/null 2>&1 || true
|
||||
die "Solver did not become reachable on 127.0.0.1:${DICE_PORT} (container stopped)."
|
||||
fi
|
||||
assert_no_edge_conflict
|
||||
}
|
||||
|
||||
# --- Phase 6: reload the edge -----------------------------------------------
|
||||
reload_caddy() {
|
||||
if docker exec "$CADDY_CONTAINER" caddy validate --config /etc/caddy/Caddyfile >/dev/null 2>&1; then
|
||||
docker exec "$CADDY_CONTAINER" caddy reload --config /etc/caddy/Caddyfile
|
||||
log_ok "Reloaded ${CADDY_CONTAINER}."
|
||||
else
|
||||
die "Caddy config invalid — aborting reload. Check ${SITES_DIR}/*.caddy."
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Phase 7: auto-rebuild on commit ----------------------------------------
|
||||
# The build runs in a memory-capped systemd user unit rather than inside the
|
||||
# git hook. MemorySwapMax=0 is required, not cosmetic: this box has ~4 GB of
|
||||
# zram (swap held IN ram), so a memory-limited cgroup without it thrashes zram
|
||||
# instead of dying cleanly, and freezes the machine. A git hook running tsc and
|
||||
# vite is precisely the scenario that costs.
|
||||
install_autorebuild() {
|
||||
ensure_dir "$UNIT_DIR"
|
||||
cat >"$UNIT_FILE" <<EOF
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
[Unit]
|
||||
Description=Rebuild the KCD2 dice solver static bundle
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
WorkingDirectory=${DICE_SRC}
|
||||
ExecStart=${DICE_BUILD}
|
||||
MemoryMax=3G
|
||||
MemorySwapMax=0
|
||||
EOF
|
||||
systemctl --user daemon-reload
|
||||
log_ok "Installed ${UNIT_FILE}."
|
||||
|
||||
[[ -d "${REPO_ROOT}/.git" ]] || die "${REPO_ROOT} is not a git repository."
|
||||
cat >"$HOOK_FILE" <<EOF
|
||||
#!/bin/bash
|
||||
# Managed by setup_kcd2_dice_solver.sh — do not edit by hand.
|
||||
# Republishes https://${DICE_DOMAIN}/ when main gains solver changes.
|
||||
[[ "\$(git rev-parse --abbrev-ref HEAD)" == main ]] || exit 0
|
||||
git diff --name-only HEAD~1 HEAD -- kcd2_dice_solver/ | grep -q . || exit 0
|
||||
# --no-block so the commit is never slowed, || true so systemd can never fail it.
|
||||
systemctl --user start --no-block ${UNIT_NAME} || true
|
||||
EOF
|
||||
chmod +x "$HOOK_FILE"
|
||||
log_ok "Installed ${HOOK_FILE}."
|
||||
}
|
||||
|
||||
# --- Phase 8: firewall ------------------------------------------------------
|
||||
# setup_wireguard_ssh.sh owns /etc/nftables.conf and regenerates it from
|
||||
# scratch — `flush ruleset` and all — on every run. That is fine on a quiet
|
||||
# machine and rude on a busy one: it momentarily drops the whole ruleset while
|
||||
# games, VPN links and long-lived connections are relying on it. Since the only
|
||||
# thing this deployment needs is 80/443 already being accepted, check first and
|
||||
# only pay for the regeneration when the rule is genuinely missing.
|
||||
# Exact token membership rather than a regex over the rule text: a loose
|
||||
# pattern would also match "8080, 4433" and wrongly conclude the ports are open.
|
||||
web_ports_open() {
|
||||
local line ports
|
||||
while read -r line; do
|
||||
# Strip everything but digits and commas, then test for whole tokens.
|
||||
ports=",${line//[^0-9,]/},"
|
||||
if [[ $ports == *,80,* && $ports == *,443,* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done < <(sudo -n nft list ruleset 2>/dev/null | grep -oE 'tcp dport \{[^}]*\}')
|
||||
return 1
|
||||
}
|
||||
|
||||
ensure_firewall() {
|
||||
if web_ports_open; then
|
||||
log_ok "Ports 80/443 already accepted — leaving the ruleset untouched."
|
||||
return 0
|
||||
fi
|
||||
log_info "Opening ports 80/443 via setup_wireguard_ssh.sh…"
|
||||
sudo bash "$WG_SCRIPT" allow-web
|
||||
}
|
||||
|
||||
# --- Phase 9: report --------------------------------------------------------
|
||||
print_report() {
|
||||
cat <<EOF
|
||||
|
||||
============================================================================
|
||||
KCD2 dice solver deployed.
|
||||
============================================================================
|
||||
Solver : https://${DICE_DOMAIN}/
|
||||
|
||||
Auto-rebuild: committing to main with changes under kcd2_dice_solver/ starts
|
||||
${UNIT_NAME}, which rebuilds dist/ in place.
|
||||
Watch it: journalctl --user -u ${UNIT_NAME} -f
|
||||
Force it: systemctl --user start ${UNIT_NAME}
|
||||
|
||||
NOTE: the rebuild builds the WORKING TREE, not the commit. An unrelated
|
||||
uncommitted edit that happens to be present when it runs will ship to the live
|
||||
site. Commit or set aside unrelated work before triggering a rebuild.
|
||||
|
||||
Acceptance test (do this on your phone):
|
||||
Turn Wi-Fi OFF (use cellular) and open https://${DICE_DOMAIN}/ —
|
||||
all 43 dice should be on one screen and tappable.
|
||||
============================================================================
|
||||
EOF
|
||||
}
|
||||
|
||||
setup_cmd() {
|
||||
print_setup_header "KCD2 dice solver setup"
|
||||
preflight
|
||||
build_solver
|
||||
write_serve_stack
|
||||
# The snippet goes into the SHARED sites/ directory, so it is written only
|
||||
# after the backend is up and verified. Writing it first means a failure in
|
||||
# start_site leaves an orphan behind that the next `caddy reload` from any
|
||||
# other service picks up — the edge then serves this domain as a 502 and
|
||||
# starts ACME retries for a host with no backend.
|
||||
start_site
|
||||
write_snippet
|
||||
reload_caddy
|
||||
install_autorebuild
|
||||
ensure_firewall
|
||||
print_report
|
||||
log_ok "KCD2 dice solver setup complete."
|
||||
}
|
||||
|
||||
# --- status: self-diagnose --------------------------------------------------
|
||||
status_line() {
|
||||
if [[ $1 -eq 0 ]]; then
|
||||
log_ok "$2"
|
||||
else
|
||||
log_warn "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
check_http() {
|
||||
# Prints 0 to stdout if the URL returns 200 with a non-empty body, else 1.
|
||||
# The body-size check matters: a host-matcher misconfiguration answers 200
|
||||
# with nothing in it, which a status-only check would call healthy.
|
||||
#
|
||||
# Certificates are NOT skipped for https URLs — `curl -k` would report an
|
||||
# expired or failed-ACME cert as a healthy site, which is precisely the kind
|
||||
# of green-while-broken this command exists to catch. The loopback check runs
|
||||
# over plain http, so it needs no exception.
|
||||
local url="$1" out code size
|
||||
out="$(curl -s -o /dev/null -w '%{http_code} %{size_download}' --max-time 10 \
|
||||
${2:+-H "Host: $2"} "$url" 2>/dev/null || echo "000 0")"
|
||||
read -r code size <<<"$out"
|
||||
[[ $code == "200" && ${size:-0} -gt 0 ]] && echo 0 || echo 1
|
||||
}
|
||||
|
||||
status_cmd() {
|
||||
print_setup_header "KCD2 dice solver status"
|
||||
|
||||
if has_cmd node; then
|
||||
status_line 0 "node present"
|
||||
else
|
||||
status_line 1 "node missing"
|
||||
fi
|
||||
if has_cmd pnpm; then
|
||||
status_line 0 "pnpm present"
|
||||
else
|
||||
status_line 1 "pnpm missing"
|
||||
fi
|
||||
if [[ -f "${DICE_SRC}/dist/index.html" ]]; then
|
||||
status_line 0 "build present (dist/index.html)"
|
||||
else
|
||||
status_line 1 "build missing — run setup"
|
||||
fi
|
||||
if docker ps --format '{{.Names}}' | grep -qx "$DICE_CONTAINER"; then
|
||||
status_line 0 "${DICE_CONTAINER} container running"
|
||||
local pid listeners
|
||||
pid="$(docker inspect -f '{{.State.Pid}}' "$DICE_CONTAINER" 2>/dev/null || echo "")"
|
||||
listeners="$(sudo -n ss -ltnp 2>/dev/null | grep "pid=${pid}," || true)"
|
||||
if grep -qE ':(80|443)\b' <<<"$listeners"; then
|
||||
status_line 1 "${DICE_CONTAINER} is CONTENDING for :80/:443 with ${CADDY_CONTAINER}"
|
||||
else
|
||||
status_line 0 "${DICE_CONTAINER} listens only on ${DICE_PORT}"
|
||||
fi
|
||||
else
|
||||
status_line 1 "${DICE_CONTAINER} container not running"
|
||||
fi
|
||||
|
||||
status_line "$(check_http "http://127.0.0.1:${DICE_PORT}/" "$DICE_DOMAIN")" \
|
||||
"local static server (127.0.0.1:${DICE_PORT}, Host: ${DICE_DOMAIN})"
|
||||
|
||||
if [[ -f $DICE_SNIPPET ]]; then
|
||||
status_line 0 "Caddy snippet present"
|
||||
else
|
||||
status_line 1 "Caddy snippet missing"
|
||||
fi
|
||||
if getent hosts "$DICE_DOMAIN" >/dev/null; then
|
||||
status_line 0 "${DICE_DOMAIN} resolves"
|
||||
else
|
||||
status_line 1 "${DICE_DOMAIN} does not resolve"
|
||||
fi
|
||||
status_line "$(check_http "https://${DICE_DOMAIN}/")" \
|
||||
"external https://${DICE_DOMAIN}/"
|
||||
|
||||
# The hook lives in .git/hooks, which is not version-controlled, so it is
|
||||
# lost on a fresh clone and must be checked rather than assumed. Spelled as
|
||||
# an explicit if: with two conditions, A && B || C really would misfire.
|
||||
if [[ -x $HOOK_FILE ]] && grep -q "$UNIT_NAME" "$HOOK_FILE" 2>/dev/null; then
|
||||
status_line 0 "post-commit auto-rebuild hook installed"
|
||||
else
|
||||
status_line 1 "post-commit hook missing — re-run setup"
|
||||
fi
|
||||
|
||||
if [[ -f $UNIT_FILE ]]; then
|
||||
status_line 0 "${UNIT_NAME} installed"
|
||||
else
|
||||
status_line 1 "${UNIT_NAME} missing — re-run setup"
|
||||
fi
|
||||
if systemctl --user is-failed --quiet "$UNIT_NAME"; then
|
||||
status_line 1 "last auto-rebuild FAILED — journalctl --user -u ${UNIT_NAME}"
|
||||
else
|
||||
status_line 0 "no failed auto-rebuild recorded"
|
||||
fi
|
||||
|
||||
# "not failed" also covers "never ran", so check the artefact rather than the
|
||||
# unit: if any source file is newer than the published bundle, the hook is
|
||||
# not firing and the site is quietly serving an old build.
|
||||
local newest_src
|
||||
newest_src="$(find "${DICE_SRC}/src" "${DICE_SRC}/index.html" -type f -newer \
|
||||
"${DICE_SRC}/dist/index.html" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n $newest_src ]]; then
|
||||
status_line 1 "published bundle is STALE (e.g. ${newest_src#"${DICE_SRC}/"}) — run: systemctl --user start ${UNIT_NAME}"
|
||||
else
|
||||
status_line 0 "published bundle is up to date with src/"
|
||||
fi
|
||||
}
|
||||
# shellcheck source=lib/kcd2_serve.sh
|
||||
source "$SCRIPT_DIR/lib/kcd2_serve.sh"
|
||||
# shellcheck source=lib/kcd2_expose.sh
|
||||
source "$SCRIPT_DIR/lib/kcd2_expose.sh"
|
||||
# shellcheck source=lib/kcd2_autorebuild.sh
|
||||
source "$SCRIPT_DIR/lib/kcd2_autorebuild.sh"
|
||||
|
||||
main() {
|
||||
local cmd="${1:-setup}"
|
||||
|
||||
@ -145,288 +145,11 @@ fix_nm_wait_online() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 2: Sysctl tuning (swappiness, VFS cache, dirty pages)
|
||||
# ===================================================================
|
||||
fix_sysctl_tuning() {
|
||||
local sysctl_file="/etc/sysctl.d/99-performance-tuning.conf"
|
||||
# shellcheck source=lib/ubuntu_perf_fixes.sh
|
||||
source "$SCRIPT_DIR/lib/ubuntu_perf_fixes.sh"
|
||||
# shellcheck source=lib/ubuntu_perf_more.sh
|
||||
source "$SCRIPT_DIR/lib/ubuntu_perf_more.sh"
|
||||
|
||||
if [[ -f $sysctl_file ]]; then
|
||||
log_ok "Sysctl performance tuning already applied — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Save current values for undo
|
||||
local cur_swappiness cur_vfs cur_dirty_ratio cur_dirty_bg
|
||||
cur_swappiness=$(sysctl -n vm.swappiness 2>/dev/null || echo 60)
|
||||
cur_vfs=$(sysctl -n vm.vfs_cache_pressure 2>/dev/null || echo 100)
|
||||
cur_dirty_ratio=$(sysctl -n vm.dirty_ratio 2>/dev/null || echo 20)
|
||||
cur_dirty_bg=$(sysctl -n vm.dirty_background_ratio 2>/dev/null || echo 10)
|
||||
|
||||
cat >"$sysctl_file" <<'SYSCTL'
|
||||
# Performance tuning for Ubuntu laptop with 32GB RAM + NVMe SSD
|
||||
# Created by fix_ubuntu_performance.sh
|
||||
#
|
||||
# vm.swappiness=10: Prefer keeping data in RAM over swapping (32GB is plenty)
|
||||
# vm.vfs_cache_pressure=50: Keep filesystem dentries/inodes cached longer (helps dev work)
|
||||
# vm.dirty_ratio=15: Allow more dirty pages before forced writeback (NVMe handles bursts)
|
||||
# vm.dirty_background_ratio=5: Start background writeback earlier for smoother I/O
|
||||
|
||||
vm.swappiness = 10
|
||||
vm.vfs_cache_pressure = 50
|
||||
vm.dirty_ratio = 15
|
||||
vm.dirty_background_ratio = 5
|
||||
SYSCTL
|
||||
|
||||
# Apply immediately
|
||||
sysctl --system >/dev/null 2>&1
|
||||
|
||||
add_undo "# Undo: Remove sysctl tuning, restore defaults"
|
||||
add_undo "rm -f /etc/sysctl.d/99-performance-tuning.conf"
|
||||
add_undo "sysctl -w vm.swappiness=$cur_swappiness vm.vfs_cache_pressure=$cur_vfs vm.dirty_ratio=$cur_dirty_ratio vm.dirty_background_ratio=$cur_dirty_bg >/dev/null"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 3: NVIDIA persistence mode via systemd service
|
||||
# ===================================================================
|
||||
fix_nvidia_persistence() {
|
||||
local service_file="/etc/systemd/system/nvidia-persistence.service"
|
||||
|
||||
# Check if persistence is already on
|
||||
if nvidia-smi -q 2>/dev/null | grep -q "Persistence Mode.*Enabled"; then
|
||||
log_ok "NVIDIA persistence mode is already enabled — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# On Ubuntu, nvidia-persistenced.service is "static" (no [Install] section)
|
||||
# and starts with --no-persistence-mode. We create a small helper service
|
||||
# that runs `nvidia-smi -pm 1` after the daemon is up.
|
||||
local helper_svc="/etc/systemd/system/nvidia-persistence-mode.service"
|
||||
|
||||
if [[ -f $helper_svc ]] && systemctl is-enabled nvidia-persistence-mode.service >/dev/null 2>&1; then
|
||||
# Already set up — just make sure it's active this boot
|
||||
if ! nvidia-smi -q 2>/dev/null | grep -q "Persistence Mode.*Enabled"; then
|
||||
systemctl start nvidia-persistence-mode.service 2>/dev/null || true
|
||||
fi
|
||||
log_ok "NVIDIA persistence mode helper already configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v nvidia-persistenced >/dev/null 2>&1; then
|
||||
# Ensure the daemon is running
|
||||
systemctl start nvidia-persistenced.service 2>/dev/null || true
|
||||
|
||||
# Create a proper service with [Install] that runs nvidia-smi -pm 1
|
||||
cat >"$helper_svc" <<'NVSVC'
|
||||
[Unit]
|
||||
Description=Enable NVIDIA Persistence Mode
|
||||
After=nvidia-persistenced.service
|
||||
Requires=nvidia-persistenced.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/nvidia-smi -pm 1
|
||||
ExecStop=/usr/bin/nvidia-smi -pm 0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
NVSVC
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now nvidia-persistence-mode.service
|
||||
|
||||
add_undo "# Undo: Remove NVIDIA persistence mode helper service"
|
||||
add_undo "systemctl disable --now nvidia-persistence-mode.service 2>/dev/null || true"
|
||||
add_undo "rm -f /etc/systemd/system/nvidia-persistence-mode.service"
|
||||
add_undo "nvidia-smi -pm 0 2>/dev/null || true"
|
||||
add_undo "systemctl daemon-reload"
|
||||
add_undo ""
|
||||
else
|
||||
# Fall back to a simple systemd service using nvidia-smi
|
||||
cat >"$service_file" <<'NVSVC'
|
||||
[Unit]
|
||||
Description=NVIDIA Persistence Mode
|
||||
After=nvidia.target
|
||||
Requires=nvidia.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/nvidia-smi -pm 1
|
||||
ExecStop=/usr/bin/nvidia-smi -pm 0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
NVSVC
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now nvidia-persistence.service
|
||||
|
||||
add_undo "# Undo: Remove NVIDIA persistence service"
|
||||
add_undo "systemctl disable --now nvidia-persistence.service 2>/dev/null || true"
|
||||
add_undo "rm -f /etc/systemd/system/nvidia-persistence.service"
|
||||
add_undo "systemctl daemon-reload"
|
||||
add_undo ""
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 4: Install and enable earlyoom
|
||||
# ===================================================================
|
||||
fix_earlyoom() {
|
||||
if systemctl is-active earlyoom.service >/dev/null 2>&1; then
|
||||
log_ok "earlyoom is already running — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! dpkg -l earlyoom 2>/dev/null | grep -q '^ii'; then
|
||||
log_info "Installing earlyoom..."
|
||||
apt-get install -y earlyoom >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Configure earlyoom: kill at 5% free RAM / 10% free swap
|
||||
local earlyoom_conf="/etc/default/earlyoom"
|
||||
if [[ -f $earlyoom_conf ]]; then
|
||||
cp "$earlyoom_conf" "${earlyoom_conf}.bak"
|
||||
fi
|
||||
|
||||
cat >"$earlyoom_conf" <<'EARLYOOM'
|
||||
# earlyoom configuration - prevent OOM hard-freezes
|
||||
# Created by fix_ubuntu_performance.sh
|
||||
# -r 5 = act when free RAM drops below 5%
|
||||
# -s 10 = act when free swap drops below 10%
|
||||
# -n = send SIGTERM first (graceful), then SIGKILL
|
||||
# --prefer="(firefox|chromium|chrome)" = prefer killing browsers (they recover well)
|
||||
EARLYOOM_ARGS="-r 5 -s 10 -n --prefer '(firefox|chromium|chrome)'"
|
||||
EARLYOOM
|
||||
|
||||
systemctl enable --now earlyoom.service
|
||||
|
||||
add_undo "# Undo: Disable and remove earlyoom"
|
||||
add_undo "systemctl disable --now earlyoom.service 2>/dev/null || true"
|
||||
add_undo "apt-get remove -y earlyoom >/dev/null 2>&1 || true"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 5: Mask failed SSSD units (not needed on non-domain laptops)
|
||||
# ===================================================================
|
||||
fix_failed_sssd() {
|
||||
local sssd_units=(
|
||||
sssd-pac.service
|
||||
sssd-nss.socket
|
||||
sssd-pac.socket
|
||||
sssd-pam-priv.socket
|
||||
sssd-pam.socket
|
||||
)
|
||||
|
||||
local any_failed=false
|
||||
for unit in "${sssd_units[@]}"; do
|
||||
if systemctl is-failed "$unit" >/dev/null 2>&1; then
|
||||
any_failed=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $any_failed == "false" ]]; then
|
||||
log_ok "No failed SSSD units — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
add_undo "# Undo: Unmask SSSD units"
|
||||
for unit in "${sssd_units[@]}"; do
|
||||
if systemctl is-failed "$unit" >/dev/null 2>&1; then
|
||||
systemctl stop "$unit" 2>/dev/null || true
|
||||
systemctl mask "$unit"
|
||||
log_info "Masked $unit"
|
||||
add_undo "systemctl unmask $unit"
|
||||
fi
|
||||
done
|
||||
|
||||
systemctl reset-failed 2>/dev/null || true
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 6: Journal vacuum + permanent size cap
|
||||
# ===================================================================
|
||||
fix_journal() {
|
||||
# Create permanent size cap via drop-in
|
||||
local dropin_dir="/etc/systemd/journald.conf.d"
|
||||
local dropin_file="$dropin_dir/size-limit.conf"
|
||||
|
||||
if [[ -f $dropin_file ]] && grep -q 'SystemMaxUse=300M' "$dropin_file"; then
|
||||
log_ok "Journal size cap already configured — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$dropin_dir"
|
||||
cat >"$dropin_file" <<'JOURNALEOF'
|
||||
[Journal]
|
||||
SystemMaxUse=300M
|
||||
JOURNALEOF
|
||||
|
||||
# Vacuum existing logs
|
||||
journalctl --vacuum-size=300M 2>/dev/null || true
|
||||
|
||||
systemctl restart systemd-journald
|
||||
|
||||
add_undo "# Undo: Remove journal size cap"
|
||||
add_undo "rm -f /etc/systemd/journald.conf.d/size-limit.conf"
|
||||
add_undo "systemctl restart systemd-journald"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 7: Disable snap-related boot slowness (optional but impactful)
|
||||
# ===================================================================
|
||||
fix_snap_startup() {
|
||||
# Disable snapd.snap-repair.timer - not critical, runs periodically
|
||||
if systemctl is-enabled snapd.snap-repair.timer >/dev/null 2>&1; then
|
||||
systemctl disable snapd.snap-repair.timer
|
||||
systemctl stop snapd.snap-repair.timer 2>/dev/null || true
|
||||
|
||||
add_undo "# Undo: Re-enable snap repair timer"
|
||||
add_undo "systemctl enable snapd.snap-repair.timer"
|
||||
add_undo ""
|
||||
else
|
||||
log_ok "snapd.snap-repair.timer already disabled — skipping."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Undo mode: run the most recent undo script
|
||||
# ===================================================================
|
||||
run_undo() {
|
||||
local latest_undo
|
||||
# shellcheck disable=SC2012
|
||||
latest_undo=$(ls -1t /root/undo_ubuntu_performance_*.sh 2>/dev/null | head -1)
|
||||
|
||||
if [[ -z ${latest_undo:-} ]]; then
|
||||
log_error "No undo script found in /root/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Running undo script: $latest_undo"
|
||||
bash "$latest_undo"
|
||||
log_ok "All changes reversed."
|
||||
log_info "Reboot recommended to ensure all changes take effect."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Apply all fixes
|
||||
# ===================================================================
|
||||
main() {
|
||||
if [[ $UNDO_MODE == "true" ]]; then
|
||||
run_undo
|
||||
|
||||
215
linux_configuration/scripts/single_use/fixes/lib/ubuntu_perf_fixes.sh
Executable file
215
linux_configuration/scripts/single_use/fixes/lib/ubuntu_perf_fixes.sh
Executable file
@ -0,0 +1,215 @@
|
||||
#!/bin/bash
|
||||
# sysctl, NVIDIA persistence and earlyoom performance fixes.
|
||||
#
|
||||
# Sourced by fix_ubuntu_performance.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# ===================================================================
|
||||
# Fix 2: Sysctl tuning (swappiness, VFS cache, dirty pages)
|
||||
# ===================================================================
|
||||
fix_sysctl_tuning() {
|
||||
local sysctl_file="/etc/sysctl.d/99-performance-tuning.conf"
|
||||
|
||||
if [[ -f $sysctl_file ]]; then
|
||||
log_ok "Sysctl performance tuning already applied — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Save current values for undo
|
||||
local cur_swappiness cur_vfs cur_dirty_ratio cur_dirty_bg
|
||||
cur_swappiness=$(sysctl -n vm.swappiness 2>/dev/null || echo 60)
|
||||
cur_vfs=$(sysctl -n vm.vfs_cache_pressure 2>/dev/null || echo 100)
|
||||
cur_dirty_ratio=$(sysctl -n vm.dirty_ratio 2>/dev/null || echo 20)
|
||||
cur_dirty_bg=$(sysctl -n vm.dirty_background_ratio 2>/dev/null || echo 10)
|
||||
|
||||
cat >"$sysctl_file" <<'SYSCTL'
|
||||
# Performance tuning for Ubuntu laptop with 32GB RAM + NVMe SSD
|
||||
# Created by fix_ubuntu_performance.sh
|
||||
#
|
||||
# vm.swappiness=10: Prefer keeping data in RAM over swapping (32GB is plenty)
|
||||
# vm.vfs_cache_pressure=50: Keep filesystem dentries/inodes cached longer (helps dev work)
|
||||
# vm.dirty_ratio=15: Allow more dirty pages before forced writeback (NVMe handles bursts)
|
||||
# vm.dirty_background_ratio=5: Start background writeback earlier for smoother I/O
|
||||
|
||||
vm.swappiness = 10
|
||||
vm.vfs_cache_pressure = 50
|
||||
vm.dirty_ratio = 15
|
||||
vm.dirty_background_ratio = 5
|
||||
SYSCTL
|
||||
|
||||
# Apply immediately
|
||||
sysctl --system >/dev/null 2>&1
|
||||
|
||||
add_undo "# Undo: Remove sysctl tuning, restore defaults"
|
||||
add_undo "rm -f /etc/sysctl.d/99-performance-tuning.conf"
|
||||
add_undo "sysctl -w vm.swappiness=$cur_swappiness vm.vfs_cache_pressure=$cur_vfs vm.dirty_ratio=$cur_dirty_ratio vm.dirty_background_ratio=$cur_dirty_bg >/dev/null"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 3: NVIDIA persistence mode via systemd service
|
||||
# ===================================================================
|
||||
fix_nvidia_persistence() {
|
||||
local service_file="/etc/systemd/system/nvidia-persistence.service"
|
||||
|
||||
# Check if persistence is already on
|
||||
if nvidia-smi -q 2>/dev/null | grep -q "Persistence Mode.*Enabled"; then
|
||||
log_ok "NVIDIA persistence mode is already enabled — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# On Ubuntu, nvidia-persistenced.service is "static" (no [Install] section)
|
||||
# and starts with --no-persistence-mode. We create a small helper service
|
||||
# that runs `nvidia-smi -pm 1` after the daemon is up.
|
||||
local helper_svc="/etc/systemd/system/nvidia-persistence-mode.service"
|
||||
|
||||
if [[ -f $helper_svc ]] && systemctl is-enabled nvidia-persistence-mode.service >/dev/null 2>&1; then
|
||||
# Already set up — just make sure it's active this boot
|
||||
if ! nvidia-smi -q 2>/dev/null | grep -q "Persistence Mode.*Enabled"; then
|
||||
systemctl start nvidia-persistence-mode.service 2>/dev/null || true
|
||||
fi
|
||||
log_ok "NVIDIA persistence mode helper already configured."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v nvidia-persistenced >/dev/null 2>&1; then
|
||||
# Ensure the daemon is running
|
||||
systemctl start nvidia-persistenced.service 2>/dev/null || true
|
||||
|
||||
# Create a proper service with [Install] that runs nvidia-smi -pm 1
|
||||
cat >"$helper_svc" <<'NVSVC'
|
||||
[Unit]
|
||||
Description=Enable NVIDIA Persistence Mode
|
||||
After=nvidia-persistenced.service
|
||||
Requires=nvidia-persistenced.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/nvidia-smi -pm 1
|
||||
ExecStop=/usr/bin/nvidia-smi -pm 0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
NVSVC
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now nvidia-persistence-mode.service
|
||||
|
||||
add_undo "# Undo: Remove NVIDIA persistence mode helper service"
|
||||
add_undo "systemctl disable --now nvidia-persistence-mode.service 2>/dev/null || true"
|
||||
add_undo "rm -f /etc/systemd/system/nvidia-persistence-mode.service"
|
||||
add_undo "nvidia-smi -pm 0 2>/dev/null || true"
|
||||
add_undo "systemctl daemon-reload"
|
||||
add_undo ""
|
||||
else
|
||||
# Fall back to a simple systemd service using nvidia-smi
|
||||
cat >"$service_file" <<'NVSVC'
|
||||
[Unit]
|
||||
Description=NVIDIA Persistence Mode
|
||||
After=nvidia.target
|
||||
Requires=nvidia.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/bin/nvidia-smi -pm 1
|
||||
ExecStop=/usr/bin/nvidia-smi -pm 0
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
NVSVC
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now nvidia-persistence.service
|
||||
|
||||
add_undo "# Undo: Remove NVIDIA persistence service"
|
||||
add_undo "systemctl disable --now nvidia-persistence.service 2>/dev/null || true"
|
||||
add_undo "rm -f /etc/systemd/system/nvidia-persistence.service"
|
||||
add_undo "systemctl daemon-reload"
|
||||
add_undo ""
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 4: Install and enable earlyoom
|
||||
# ===================================================================
|
||||
fix_earlyoom() {
|
||||
if systemctl is-active earlyoom.service >/dev/null 2>&1; then
|
||||
log_ok "earlyoom is already running — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! dpkg -l earlyoom 2>/dev/null | grep -q '^ii'; then
|
||||
log_info "Installing earlyoom..."
|
||||
apt-get install -y earlyoom >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# Configure earlyoom: kill at 5% free RAM / 10% free swap
|
||||
local earlyoom_conf="/etc/default/earlyoom"
|
||||
if [[ -f $earlyoom_conf ]]; then
|
||||
cp "$earlyoom_conf" "${earlyoom_conf}.bak"
|
||||
fi
|
||||
|
||||
cat >"$earlyoom_conf" <<'EARLYOOM'
|
||||
# earlyoom configuration - prevent OOM hard-freezes
|
||||
# Created by fix_ubuntu_performance.sh
|
||||
# -r 5 = act when free RAM drops below 5%
|
||||
# -s 10 = act when free swap drops below 10%
|
||||
# -n = send SIGTERM first (graceful), then SIGKILL
|
||||
# --prefer="(firefox|chromium|chrome)" = prefer killing browsers (they recover well)
|
||||
EARLYOOM_ARGS="-r 5 -s 10 -n --prefer '(firefox|chromium|chrome)'"
|
||||
EARLYOOM
|
||||
|
||||
systemctl enable --now earlyoom.service
|
||||
|
||||
add_undo "# Undo: Disable and remove earlyoom"
|
||||
add_undo "systemctl disable --now earlyoom.service 2>/dev/null || true"
|
||||
add_undo "apt-get remove -y earlyoom >/dev/null 2>&1 || true"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 5: Mask failed SSSD units (not needed on non-domain laptops)
|
||||
# ===================================================================
|
||||
fix_failed_sssd() {
|
||||
local sssd_units=(
|
||||
sssd-pac.service
|
||||
sssd-nss.socket
|
||||
sssd-pac.socket
|
||||
sssd-pam-priv.socket
|
||||
sssd-pam.socket
|
||||
)
|
||||
|
||||
local any_failed=false
|
||||
for unit in "${sssd_units[@]}"; do
|
||||
if systemctl is-failed "$unit" >/dev/null 2>&1; then
|
||||
any_failed=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $any_failed == "false" ]]; then
|
||||
log_ok "No failed SSSD units — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
add_undo "# Undo: Unmask SSSD units"
|
||||
for unit in "${sssd_units[@]}"; do
|
||||
if systemctl is-failed "$unit" >/dev/null 2>&1; then
|
||||
systemctl stop "$unit" 2>/dev/null || true
|
||||
systemctl mask "$unit"
|
||||
log_info "Masked $unit"
|
||||
add_undo "systemctl unmask $unit"
|
||||
fi
|
||||
done
|
||||
|
||||
systemctl reset-failed 2>/dev/null || true
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
80
linux_configuration/scripts/single_use/fixes/lib/ubuntu_perf_more.sh
Executable file
80
linux_configuration/scripts/single_use/fixes/lib/ubuntu_perf_more.sh
Executable file
@ -0,0 +1,80 @@
|
||||
#!/bin/bash
|
||||
# Journal, sssd and snap-startup performance fixes.
|
||||
#
|
||||
# Sourced by fix_ubuntu_performance.sh; split out to keep ubuntu_perf_fixes.sh
|
||||
# under the 250-line cap. Sourced rather than run, so it inherits the
|
||||
# caller's strict mode and the variables above the source line.
|
||||
|
||||
# ===================================================================
|
||||
# Fix 6: Journal vacuum + permanent size cap
|
||||
# ===================================================================
|
||||
fix_journal() {
|
||||
# Create permanent size cap via drop-in
|
||||
local dropin_dir="/etc/systemd/journald.conf.d"
|
||||
local dropin_file="$dropin_dir/size-limit.conf"
|
||||
|
||||
if [[ -f $dropin_file ]] && grep -q 'SystemMaxUse=300M' "$dropin_file"; then
|
||||
log_ok "Journal size cap already configured — skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$dropin_dir"
|
||||
cat >"$dropin_file" <<'JOURNALEOF'
|
||||
[Journal]
|
||||
SystemMaxUse=300M
|
||||
JOURNALEOF
|
||||
|
||||
# Vacuum existing logs
|
||||
journalctl --vacuum-size=300M 2>/dev/null || true
|
||||
|
||||
systemctl restart systemd-journald
|
||||
|
||||
add_undo "# Undo: Remove journal size cap"
|
||||
add_undo "rm -f /etc/systemd/journald.conf.d/size-limit.conf"
|
||||
add_undo "systemctl restart systemd-journald"
|
||||
add_undo ""
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Fix 7: Disable snap-related boot slowness (optional but impactful)
|
||||
# ===================================================================
|
||||
fix_snap_startup() {
|
||||
# Disable snapd.snap-repair.timer - not critical, runs periodically
|
||||
if systemctl is-enabled snapd.snap-repair.timer >/dev/null 2>&1; then
|
||||
systemctl disable snapd.snap-repair.timer
|
||||
systemctl stop snapd.snap-repair.timer 2>/dev/null || true
|
||||
|
||||
add_undo "# Undo: Re-enable snap repair timer"
|
||||
add_undo "systemctl enable snapd.snap-repair.timer"
|
||||
add_undo ""
|
||||
else
|
||||
log_ok "snapd.snap-repair.timer already disabled — skipping."
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Undo mode: run the most recent undo script
|
||||
# ===================================================================
|
||||
run_undo() {
|
||||
local latest_undo
|
||||
# shellcheck disable=SC2012
|
||||
latest_undo=$(ls -1t /root/undo_ubuntu_performance_*.sh 2>/dev/null | head -1)
|
||||
|
||||
if [[ -z ${latest_undo:-} ]]; then
|
||||
log_error "No undo script found in /root/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Running undo script: $latest_undo"
|
||||
bash "$latest_undo"
|
||||
log_ok "All changes reversed."
|
||||
log_info "Reboot recommended to ensure all changes take effect."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ===================================================================
|
||||
# Apply all fixes
|
||||
# ===================================================================
|
||||
29
linux_configuration/scripts/single_use/utils/lib/download_classify.sh
Executable file
29
linux_configuration/scripts/single_use/utils/lib/download_classify.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Media-type classification for the download sweep.
|
||||
#
|
||||
# Sourced by organize_downloads.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# Function to check if file has media extension (zero forks: bash 4 ${var,,})
|
||||
is_media_file() {
|
||||
local file="$1"
|
||||
local extension="${file##*.}"
|
||||
extension="${extension,,}"
|
||||
|
||||
# Check if it's an image
|
||||
for ext in "${IMAGE_EXTENSIONS[@]}"; do
|
||||
if [[ $extension == "$ext" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if it's a video
|
||||
for ext in "${VIDEO_EXTENSIONS[@]}"; do
|
||||
if [[ $extension == "$ext" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
203
linux_configuration/scripts/single_use/utils/lib/download_media.sh
Executable file
203
linux_configuration/scripts/single_use/utils/lib/download_media.sh
Executable file
@ -0,0 +1,203 @@
|
||||
#!/bin/bash
|
||||
# Media discovery, archiving and the per-category moves.
|
||||
#
|
||||
# Sourced by organize_downloads.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# Function to find media files in a directory (non-recursive for home, avoid common system dirs)
|
||||
find_media_files() {
|
||||
local search_dir="$1"
|
||||
local files=()
|
||||
# Directories to exclude under Downloads
|
||||
local -a EXCLUDES=(
|
||||
".git" ".hg" ".svn" ".cache" "node_modules" "dist" "build" "out" "target" "coverage" "__pycache__" "venv" ".venv"
|
||||
# previous staging dirs created by this script
|
||||
".media_organize_" "media_organize_"
|
||||
# too_big folder for oversized files
|
||||
"too_big"
|
||||
)
|
||||
|
||||
if [[ $search_dir == "$HOME_DIR" ]]; then
|
||||
# For home directory, only check files directly in ~ (not subdirectories)
|
||||
# Exclude common system/config directories
|
||||
while IFS= read -r -d '' file; do
|
||||
local basename
|
||||
basename=$(basename "$file")
|
||||
# Skip hidden files and common system directories
|
||||
if [[ ! $basename =~ ^\. ]] && [[ -f $file ]]; then
|
||||
if is_media_file "$file"; then
|
||||
files+=("$file")
|
||||
fi
|
||||
fi
|
||||
done < <(find "$search_dir" -maxdepth 1 -type f -print0 2>/dev/null)
|
||||
else
|
||||
# For Downloads, search recursively, pruning excluded directories
|
||||
# Build prune expression
|
||||
local prune_expr=()
|
||||
for ex in "${EXCLUDES[@]}"; do
|
||||
prune_expr+=(-name "$ex*" -o)
|
||||
done
|
||||
# Remove trailing -o
|
||||
unset 'prune_expr[${#prune_expr[@]}-1]'
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
if is_media_file "$file"; then
|
||||
files+=("$file")
|
||||
fi
|
||||
done < <(find "$search_dir" \( -type d \( "${prune_expr[@]}" \) -prune \) -o -type f -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${files[@]}"
|
||||
}
|
||||
|
||||
# Function to create timestamped zip archive
|
||||
create_media_archive() {
|
||||
local files=("$@")
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
log "No media files found to archive."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create timestamp for archive name
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local archive_name="media_archive_${timestamp}.zip"
|
||||
local archive_path="$DOWNLOADS_DIR/$archive_name"
|
||||
|
||||
# Create temporary directory (fallback to /tmp if needed)
|
||||
if ! mkdir -p "$TEMP_DIR" 2>/dev/null; then
|
||||
TEMP_DIR="/tmp/media_organize_$$"
|
||||
mkdir -p "$TEMP_DIR"
|
||||
fi
|
||||
|
||||
# Ensure temp dir is cleaned up on function return; trap unsets itself after running
|
||||
trap 'rm -rf "$TEMP_DIR" 2>/dev/null || true; trap - RETURN' RETURN
|
||||
|
||||
log "Found ${#files[@]} media files to archive."
|
||||
log "Creating archive: $archive_path"
|
||||
|
||||
# Copy files to temp directory maintaining relative structure
|
||||
local successfully_copied=()
|
||||
local copy_errors=0
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
local relative_path=""
|
||||
if [[ $file == "$DOWNLOADS_DIR"* ]]; then
|
||||
relative_path="downloads/${file#"$DOWNLOADS_DIR"/}"
|
||||
else
|
||||
relative_path="home/${file#"$HOME_DIR"/}"
|
||||
fi
|
||||
|
||||
local temp_file="$TEMP_DIR/$relative_path"
|
||||
local temp_dir
|
||||
temp_dir=$(dirname "$temp_file")
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
# Check readability first to provide a clearer error
|
||||
if [[ ! -r $file ]]; then
|
||||
log "WARNING: Cannot read $file (permission denied?)"
|
||||
((copy_errors++)) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Attempt copy and capture any error for logging
|
||||
local cp_err
|
||||
if cp_err=$(cp -p "$file" "$temp_file" 2>&1); then
|
||||
successfully_copied+=("$file")
|
||||
else
|
||||
# Surface the cp error so the user can see the reason
|
||||
log "WARNING: Failed to copy $file -> $cp_err"
|
||||
# Special hint for space issues
|
||||
if echo "$cp_err" | grep -qi "No space left on device"; then
|
||||
log "HINT: Not enough free space to stage files. Using $TEMP_DIR. Free up space or change TEMP_DIR."
|
||||
fi
|
||||
((copy_errors++)) || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#successfully_copied[@]} -eq 0 ]]; then
|
||||
log "ERROR: No files were successfully copied to temp directory."
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $copy_errors -gt 0 ]]; then
|
||||
log "WARNING: $copy_errors files failed to copy."
|
||||
fi
|
||||
|
||||
# Create zip archive with maximum compression
|
||||
log "Creating zip archive with ${#successfully_copied[@]} files..."
|
||||
cd "$TEMP_DIR"
|
||||
if zip -9 -r "$archive_path" . 2>&1; then
|
||||
log "Successfully created archive with ${#successfully_copied[@]} files."
|
||||
|
||||
# Verify the zip file was actually created and is not empty
|
||||
if [[ ! -f $archive_path ]]; then
|
||||
log "ERROR: Archive file was not created at $archive_path"
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local archive_size
|
||||
archive_size=$(stat -c%s "$archive_path" 2>/dev/null || echo "0")
|
||||
if [[ $archive_size -eq 0 ]]; then
|
||||
log "ERROR: Archive file is empty"
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remove original files only if zip was successful
|
||||
local removed_count=0
|
||||
local remove_errors=0
|
||||
|
||||
log "Starting to remove ${#successfully_copied[@]} original files..."
|
||||
|
||||
# Temporarily disable strict error handling for file removal
|
||||
set +e
|
||||
|
||||
for file in "${successfully_copied[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
if rm "$file" 2>/dev/null; then
|
||||
removed_count=$((removed_count + 1))
|
||||
log "Removed: $(basename "$file")"
|
||||
else
|
||||
remove_errors=$((remove_errors + 1))
|
||||
log "ERROR: Failed to remove $(basename "$file")"
|
||||
fi
|
||||
else
|
||||
log "WARNING: File no longer exists: $(basename "$file")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Re-enable strict error handling
|
||||
set -e
|
||||
|
||||
log "Successfully removed $removed_count original files."
|
||||
if [[ $remove_errors -gt 0 ]]; then
|
||||
log "WARNING: Failed to remove $remove_errors files."
|
||||
fi
|
||||
log "Archive size: $(du -h "$archive_path" | cut -f1)"
|
||||
|
||||
# Cleanup temp directory (trap will also attempt, which is safe)
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Return success only if we removed files or there were no files to remove
|
||||
if [[ $removed_count -gt 0 ]] || [[ ${#successfully_copied[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
else
|
||||
log "ERROR: Failed to remove any files after successful archive creation."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "ERROR: Failed to create archive. Original files preserved."
|
||||
log "Zip command failed."
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
59
linux_configuration/scripts/single_use/utils/lib/download_sizes.sh
Executable file
59
linux_configuration/scripts/single_use/utils/lib/download_sizes.sh
Executable file
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
# Size thresholds and the oversized-file moves.
|
||||
#
|
||||
# Sourced by organize_downloads.sh; split out to keep it under the 250-line cap.
|
||||
# Sourced rather than run, so it inherits the caller's strict mode and
|
||||
# the helper functions and variables defined above the source line.
|
||||
|
||||
# Function to check if file is too big for archiving
|
||||
is_too_big() {
|
||||
local file="$1"
|
||||
local size
|
||||
size=$(stat -c%s "$file" 2>/dev/null || echo "0")
|
||||
[[ $size -gt $SIZE_THRESHOLD ]]
|
||||
}
|
||||
|
||||
# Function to move oversized files to too_big directory
|
||||
move_big_files() {
|
||||
local files=("$@")
|
||||
local moved_count=0
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create too_big directory if it doesn't exist
|
||||
mkdir -p "$TOO_BIG_DIR"
|
||||
|
||||
log "Moving ${#files[@]} oversized files to $TOO_BIG_DIR..."
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
local basename
|
||||
basename=$(basename "$file")
|
||||
local dest="$TOO_BIG_DIR/$basename"
|
||||
|
||||
# Handle filename collision
|
||||
if [[ -f $dest ]]; then
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local name="${basename%.*}"
|
||||
local ext="${basename##*.}"
|
||||
if [[ $name == "$ext" ]]; then
|
||||
dest="$TOO_BIG_DIR/${name}_${timestamp}"
|
||||
else
|
||||
dest="$TOO_BIG_DIR/${name}_${timestamp}.${ext}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if mv "$file" "$dest" 2>/dev/null; then
|
||||
log "Moved (too big): $(basename "$file") -> $dest"
|
||||
moved_count=$((moved_count + 1))
|
||||
else
|
||||
log "ERROR: Failed to move $(basename "$file")"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "Successfully moved $moved_count oversized files."
|
||||
}
|
||||
@ -67,278 +67,14 @@ while [[ ${1:-} =~ ^- ]]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# Function to check if file has media extension (zero forks: bash 4 ${var,,})
|
||||
is_media_file() {
|
||||
local file="$1"
|
||||
local extension="${file##*.}"
|
||||
extension="${extension,,}"
|
||||
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
|
||||
# shellcheck source=lib/download_classify.sh
|
||||
source "$SCRIPT_DIR/lib/download_classify.sh"
|
||||
# shellcheck source=lib/download_sizes.sh
|
||||
source "$SCRIPT_DIR/lib/download_sizes.sh"
|
||||
# shellcheck source=lib/download_media.sh
|
||||
source "$SCRIPT_DIR/lib/download_media.sh"
|
||||
|
||||
# Check if it's an image
|
||||
for ext in "${IMAGE_EXTENSIONS[@]}"; do
|
||||
if [[ $extension == "$ext" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
# Check if it's a video
|
||||
for ext in "${VIDEO_EXTENSIONS[@]}"; do
|
||||
if [[ $extension == "$ext" ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to check if file is too big for archiving
|
||||
is_too_big() {
|
||||
local file="$1"
|
||||
local size
|
||||
size=$(stat -c%s "$file" 2>/dev/null || echo "0")
|
||||
[[ $size -gt $SIZE_THRESHOLD ]]
|
||||
}
|
||||
|
||||
# Function to move oversized files to too_big directory
|
||||
move_big_files() {
|
||||
local files=("$@")
|
||||
local moved_count=0
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create too_big directory if it doesn't exist
|
||||
mkdir -p "$TOO_BIG_DIR"
|
||||
|
||||
log "Moving ${#files[@]} oversized files to $TOO_BIG_DIR..."
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
local basename
|
||||
basename=$(basename "$file")
|
||||
local dest="$TOO_BIG_DIR/$basename"
|
||||
|
||||
# Handle filename collision
|
||||
if [[ -f $dest ]]; then
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local name="${basename%.*}"
|
||||
local ext="${basename##*.}"
|
||||
if [[ $name == "$ext" ]]; then
|
||||
dest="$TOO_BIG_DIR/${name}_${timestamp}"
|
||||
else
|
||||
dest="$TOO_BIG_DIR/${name}_${timestamp}.${ext}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if mv "$file" "$dest" 2>/dev/null; then
|
||||
log "Moved (too big): $(basename "$file") -> $dest"
|
||||
moved_count=$((moved_count + 1))
|
||||
else
|
||||
log "ERROR: Failed to move $(basename "$file")"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
log "Successfully moved $moved_count oversized files."
|
||||
}
|
||||
|
||||
# Function to find media files in a directory (non-recursive for home, avoid common system dirs)
|
||||
find_media_files() {
|
||||
local search_dir="$1"
|
||||
local files=()
|
||||
# Directories to exclude under Downloads
|
||||
local -a EXCLUDES=(
|
||||
".git" ".hg" ".svn" ".cache" "node_modules" "dist" "build" "out" "target" "coverage" "__pycache__" "venv" ".venv"
|
||||
# previous staging dirs created by this script
|
||||
".media_organize_" "media_organize_"
|
||||
# too_big folder for oversized files
|
||||
"too_big"
|
||||
)
|
||||
|
||||
if [[ $search_dir == "$HOME_DIR" ]]; then
|
||||
# For home directory, only check files directly in ~ (not subdirectories)
|
||||
# Exclude common system/config directories
|
||||
while IFS= read -r -d '' file; do
|
||||
local basename
|
||||
basename=$(basename "$file")
|
||||
# Skip hidden files and common system directories
|
||||
if [[ ! $basename =~ ^\. ]] && [[ -f $file ]]; then
|
||||
if is_media_file "$file"; then
|
||||
files+=("$file")
|
||||
fi
|
||||
fi
|
||||
done < <(find "$search_dir" -maxdepth 1 -type f -print0 2>/dev/null)
|
||||
else
|
||||
# For Downloads, search recursively, pruning excluded directories
|
||||
# Build prune expression
|
||||
local prune_expr=()
|
||||
for ex in "${EXCLUDES[@]}"; do
|
||||
prune_expr+=(-name "$ex*" -o)
|
||||
done
|
||||
# Remove trailing -o
|
||||
unset 'prune_expr[${#prune_expr[@]}-1]'
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
if is_media_file "$file"; then
|
||||
files+=("$file")
|
||||
fi
|
||||
done < <(find "$search_dir" \( -type d \( "${prune_expr[@]}" \) -prune \) -o -type f -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${files[@]}"
|
||||
}
|
||||
|
||||
# Function to create timestamped zip archive
|
||||
create_media_archive() {
|
||||
local files=("$@")
|
||||
|
||||
if [[ ${#files[@]} -eq 0 ]]; then
|
||||
log "No media files found to archive."
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Create timestamp for archive name
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y%m%d_%H%M%S')
|
||||
local archive_name="media_archive_${timestamp}.zip"
|
||||
local archive_path="$DOWNLOADS_DIR/$archive_name"
|
||||
|
||||
# Create temporary directory (fallback to /tmp if needed)
|
||||
if ! mkdir -p "$TEMP_DIR" 2>/dev/null; then
|
||||
TEMP_DIR="/tmp/media_organize_$$"
|
||||
mkdir -p "$TEMP_DIR"
|
||||
fi
|
||||
|
||||
# Ensure temp dir is cleaned up on function return; trap unsets itself after running
|
||||
trap 'rm -rf "$TEMP_DIR" 2>/dev/null || true; trap - RETURN' RETURN
|
||||
|
||||
log "Found ${#files[@]} media files to archive."
|
||||
log "Creating archive: $archive_path"
|
||||
|
||||
# Copy files to temp directory maintaining relative structure
|
||||
local successfully_copied=()
|
||||
local copy_errors=0
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
local relative_path=""
|
||||
if [[ $file == "$DOWNLOADS_DIR"* ]]; then
|
||||
relative_path="downloads/${file#"$DOWNLOADS_DIR"/}"
|
||||
else
|
||||
relative_path="home/${file#"$HOME_DIR"/}"
|
||||
fi
|
||||
|
||||
local temp_file="$TEMP_DIR/$relative_path"
|
||||
local temp_dir
|
||||
temp_dir=$(dirname "$temp_file")
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
# Check readability first to provide a clearer error
|
||||
if [[ ! -r $file ]]; then
|
||||
log "WARNING: Cannot read $file (permission denied?)"
|
||||
((copy_errors++)) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
# Attempt copy and capture any error for logging
|
||||
local cp_err
|
||||
if cp_err=$(cp -p "$file" "$temp_file" 2>&1); then
|
||||
successfully_copied+=("$file")
|
||||
else
|
||||
# Surface the cp error so the user can see the reason
|
||||
log "WARNING: Failed to copy $file -> $cp_err"
|
||||
# Special hint for space issues
|
||||
if echo "$cp_err" | grep -qi "No space left on device"; then
|
||||
log "HINT: Not enough free space to stage files. Using $TEMP_DIR. Free up space or change TEMP_DIR."
|
||||
fi
|
||||
((copy_errors++)) || true
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#successfully_copied[@]} -eq 0 ]]; then
|
||||
log "ERROR: No files were successfully copied to temp directory."
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $copy_errors -gt 0 ]]; then
|
||||
log "WARNING: $copy_errors files failed to copy."
|
||||
fi
|
||||
|
||||
# Create zip archive with maximum compression
|
||||
log "Creating zip archive with ${#successfully_copied[@]} files..."
|
||||
cd "$TEMP_DIR"
|
||||
if zip -9 -r "$archive_path" . 2>&1; then
|
||||
log "Successfully created archive with ${#successfully_copied[@]} files."
|
||||
|
||||
# Verify the zip file was actually created and is not empty
|
||||
if [[ ! -f $archive_path ]]; then
|
||||
log "ERROR: Archive file was not created at $archive_path"
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local archive_size
|
||||
archive_size=$(stat -c%s "$archive_path" 2>/dev/null || echo "0")
|
||||
if [[ $archive_size -eq 0 ]]; then
|
||||
log "ERROR: Archive file is empty"
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Remove original files only if zip was successful
|
||||
local removed_count=0
|
||||
local remove_errors=0
|
||||
|
||||
log "Starting to remove ${#successfully_copied[@]} original files..."
|
||||
|
||||
# Temporarily disable strict error handling for file removal
|
||||
set +e
|
||||
|
||||
for file in "${successfully_copied[@]}"; do
|
||||
if [[ -f $file ]]; then
|
||||
if rm "$file" 2>/dev/null; then
|
||||
removed_count=$((removed_count + 1))
|
||||
log "Removed: $(basename "$file")"
|
||||
else
|
||||
remove_errors=$((remove_errors + 1))
|
||||
log "ERROR: Failed to remove $(basename "$file")"
|
||||
fi
|
||||
else
|
||||
log "WARNING: File no longer exists: $(basename "$file")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Re-enable strict error handling
|
||||
set -e
|
||||
|
||||
log "Successfully removed $removed_count original files."
|
||||
if [[ $remove_errors -gt 0 ]]; then
|
||||
log "WARNING: Failed to remove $remove_errors files."
|
||||
fi
|
||||
log "Archive size: $(du -h "$archive_path" | cut -f1)"
|
||||
|
||||
# Cleanup temp directory (trap will also attempt, which is safe)
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Return success only if we removed files or there were no files to remove
|
||||
if [[ $removed_count -gt 0 ]] || [[ ${#successfully_copied[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
else
|
||||
log "ERROR: Failed to remove any files after successful archive creation."
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log "ERROR: Failed to create archive. Original files preserved."
|
||||
log "Zip command failed."
|
||||
rm -rf "$TEMP_DIR"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
log "Starting media file organization..."
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user