mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 21:54:00 +01:00
TachiyomiSY 1.13.2 can report a restore as SUCCESS while writing almost nothing, then sync that near-empty library up and replace the only good copy. On 2026-08-09 a 14 MB payload holding 2182 manga became a 267 KB stub, and the real library survived only as stale pages in the SQLite write-ahead log. The upstream bug is Kotlin in an APK and cannot be fixed here, so this targets the fixable half: decode the payload, count what is actually in it, and refuse to treat a collapse as normal drift. A pure is-it-zero check would have caught this incident but not the harder case upstream describes, where a restore keeps some entries and drops the rest, so the threshold is proportional. Categories get their own rule because all 2182 manga survived the round trip and all 18 categories did not. Adds the setup script and a runbook covering the WAL-page salvage used to recover the library on the day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J
124 lines
3.6 KiB
Bash
Executable File
124 lines
3.6 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# Install the SyncYomi library integrity guard as a user systemd timer.
|
|
#
|
|
# TachiyomiSY 1.13.2 can finish a backup restore, report success, and have
|
|
# written almost nothing (upstream jobobby04/TachiyomiSY#1634, #1638). The app
|
|
# then syncs that near-empty library up, replacing the good server copy. On
|
|
# 2026-08-09 the real library survived only as stale pages in the SQLite WAL.
|
|
#
|
|
# The guard cannot fix the app. It makes the silent case loud, and keeps
|
|
# restorable .tachibk snapshots so recovery never again depends on WAL forensics.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_NAME="$(basename "$0")"
|
|
readonly SCRIPT_NAME
|
|
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
|
|
readonly REPO_ROOT
|
|
readonly UNIT_DIR="${HOME}/.config/systemd/user"
|
|
readonly SERVICE_NAME="syncyomi-guard.service"
|
|
readonly TIMER_NAME="syncyomi-guard.timer"
|
|
readonly SNAPSHOT_DIR="${HOME}/syncyomi/snapshots"
|
|
readonly STATE_DIR="${HOME}/.local/share/syncyomi_guard"
|
|
readonly DB_PATH="${HOME}/syncyomi/config/syncyomi.db"
|
|
|
|
log_info() { echo "[*] $*"; }
|
|
log_ok() { echo "[+] $*"; }
|
|
die() {
|
|
echo "[!] $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: ${SCRIPT_NAME} [--uninstall]"
|
|
echo " Installs a user timer that verifies the SyncYomi payload every 30 min."
|
|
exit 0
|
|
}
|
|
|
|
require_python() {
|
|
command -v python3 >/dev/null 2>&1 ||
|
|
die "python3 not found — install it with: sudo pacman -S python"
|
|
}
|
|
|
|
# The guard reads the database through sudo-less paths, so it must be able to
|
|
# open it. A root-owned db (the container writes as root) is readable only if
|
|
# the mode allows it; check now rather than failing silently on the first timer.
|
|
check_readable() {
|
|
[[ -f ${DB_PATH} ]] || die "no SyncYomi database at ${DB_PATH}"
|
|
if ! head -c 1 "${DB_PATH}" >/dev/null 2>&1; then
|
|
die "cannot read ${DB_PATH} as $(whoami) — run: sudo chmod o+r '${DB_PATH}'"
|
|
fi
|
|
}
|
|
|
|
write_units() {
|
|
mkdir -p "${UNIT_DIR}" "${SNAPSHOT_DIR}" "${STATE_DIR}"
|
|
|
|
cat >"${UNIT_DIR}/${SERVICE_NAME}" <<EOF
|
|
# Generated by ${SCRIPT_NAME} — do not edit by hand.
|
|
[Unit]
|
|
Description=Verify the SyncYomi library payload has not silently collapsed
|
|
Documentation=https://github.com/jobobby04/TachiyomiSY/issues/1634
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
WorkingDirectory=${REPO_ROOT}
|
|
Environment=PYTHONPATH=${REPO_ROOT}
|
|
ExecStart=/usr/bin/python3 -m python_pkg.syncyomi_guard \\
|
|
--db ${DB_PATH} \\
|
|
--state-dir ${STATE_DIR} \\
|
|
--snapshot-dir ${SNAPSHOT_DIR}
|
|
EOF
|
|
|
|
cat >"${UNIT_DIR}/${TIMER_NAME}" <<EOF
|
|
# Generated by ${SCRIPT_NAME} — do not edit by hand.
|
|
[Unit]
|
|
Description=Periodic SyncYomi library integrity check
|
|
|
|
[Timer]
|
|
OnCalendar=*:00/30
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
log_ok "Wrote ${UNIT_DIR}/${SERVICE_NAME} and ${TIMER_NAME}"
|
|
}
|
|
|
|
install_timer() {
|
|
require_python
|
|
check_readable
|
|
write_units
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now "${TIMER_NAME}"
|
|
log_ok "Timer enabled (every 30 minutes)"
|
|
|
|
log_info "Running one check now"
|
|
if systemctl --user start "${SERVICE_NAME}"; then
|
|
systemctl --user status "${SERVICE_NAME}" --no-pager -n 10 || true
|
|
else
|
|
die "first run failed — inspect: journalctl --user -u ${SERVICE_NAME}"
|
|
fi
|
|
}
|
|
|
|
uninstall_timer() {
|
|
systemctl --user disable --now "${TIMER_NAME}" 2>/dev/null || true
|
|
rm -f "${UNIT_DIR}/${SERVICE_NAME}" "${UNIT_DIR}/${TIMER_NAME}"
|
|
systemctl --user daemon-reload
|
|
log_ok "Removed timer and service (snapshots in ${SNAPSHOT_DIR} kept)"
|
|
}
|
|
|
|
main() {
|
|
case "${1:-}" in
|
|
--uninstall) uninstall_timer ;;
|
|
-h | --help) usage ;;
|
|
"") install_timer ;;
|
|
*) die "Unknown option: $1" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|