181 lines
6.2 KiB
Bash
Executable File
181 lines
6.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
# Downloads Auto-Uploader — macOS launchd installer
|
||
# Runs downloads_uploader.py every 5 minutes in the background.
|
||
#
|
||
# Usage:
|
||
# ./downloads_uploader_install.sh install # register & start
|
||
# ./downloads_uploader_install.sh uninstall # stop & remove
|
||
# ./downloads_uploader_install.sh status # show current state
|
||
# ./downloads_uploader_install.sh run # manual one-shot run
|
||
# ═══════════════════════════════════════════════════════════════════
|
||
|
||
set -euo pipefail
|
||
|
||
LABEL="com.user.downloads-uploader"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
SCAN_DIR="$(dirname "$SCRIPT_DIR")" # parent of uploader/ = Downloads/
|
||
PYTHON_SCRIPT="$SCRIPT_DIR/downloads_uploader.py"
|
||
PLIST_DIR="$HOME/Library/LaunchAgents"
|
||
PLIST_FILE="$PLIST_DIR/$LABEL.plist"
|
||
LOG_OUT="$SCRIPT_DIR/uploader_stdout.log"
|
||
LOG_ERR="$SCRIPT_DIR/uploader_stderr.log"
|
||
INTERVAL=300 # seconds (5 minutes)
|
||
|
||
# macOS shows the executable name in permission dialogs, not the script name.
|
||
# To display "downloads-uploader" instead of "python3.13", we create a symlink
|
||
# named "downloads-uploader" pointing to the real python3 binary, and use that
|
||
# symlink in the launchd plist.
|
||
SYMLINK_DIR="$HOME/.local/bin"
|
||
SYMLINK="$SYMLINK_DIR/downloads-uploader"
|
||
|
||
# Detect python3
|
||
PYTHON=$(command -v python3 || command -v python || echo "")
|
||
if [[ -z "$PYTHON" ]]; then
|
||
echo "❌ python3 not found. Install it: brew install python"
|
||
exit 1
|
||
fi
|
||
|
||
# ───────────────────────────────────────────────
|
||
generate_plist() {
|
||
cat <<PLIST
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>Label</key>
|
||
<string>$LABEL</string>
|
||
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>$SYMLINK</string>
|
||
<string>$PYTHON_SCRIPT</string>
|
||
</array>
|
||
|
||
<key>StartInterval</key>
|
||
<integer>$INTERVAL</integer>
|
||
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
|
||
<!-- Pin the working directory so __file__ resolves correctly -->
|
||
<key>WorkingDirectory</key>
|
||
<string>$SCRIPT_DIR</string>
|
||
|
||
<key>StandardOutPath</key>
|
||
<string>$LOG_OUT</string>
|
||
|
||
<key>StandardErrorPath</key>
|
||
<string>$LOG_ERR</string>
|
||
|
||
<key>KeepAlive</key>
|
||
<false/>
|
||
</dict>
|
||
</plist>
|
||
PLIST
|
||
}
|
||
|
||
# ───────────────────────────────────────────────
|
||
cmd_install() {
|
||
echo "📦 Installing Downloads Auto-Uploader..."
|
||
|
||
if [[ ! -f "$PYTHON_SCRIPT" ]]; then
|
||
echo "❌ Script not found: $PYTHON_SCRIPT"
|
||
exit 1
|
||
fi
|
||
|
||
# Create named symlink so macOS shows "downloads-uploader" in permission dialogs
|
||
mkdir -p "$SYMLINK_DIR"
|
||
if [[ -L "$SYMLINK" || -e "$SYMLINK" ]]; then
|
||
rm -f "$SYMLINK"
|
||
fi
|
||
ln -s "$PYTHON" "$SYMLINK"
|
||
echo "✅ Symlink created: $SYMLINK → $PYTHON"
|
||
echo " (macOS permission dialogs will now show 'downloads-uploader')"
|
||
|
||
# Write the absolute scan_directory into the config so launchd's cwd=/
|
||
# never affects path resolution inside the Python script.
|
||
"$PYTHON" - "$SCRIPT_DIR" "$SCAN_DIR" <<'PYEOF'
|
||
import json, sys
|
||
cfg_path = sys.argv[1] + "/downloads_uploader_config.json"
|
||
scan_dir = sys.argv[2]
|
||
with open(cfg_path) as f:
|
||
cfg = json.load(f)
|
||
cfg["scan_directory"] = scan_dir
|
||
with open(cfg_path, "w") as f:
|
||
json.dump(cfg, f, indent=2, ensure_ascii=False)
|
||
print(f" scan_directory set to: {scan_dir}")
|
||
PYEOF
|
||
echo "✅ Config updated with absolute scan_directory"
|
||
|
||
mkdir -p "$PLIST_DIR"
|
||
generate_plist > "$PLIST_FILE"
|
||
echo "✅ Plist written: $PLIST_FILE"
|
||
|
||
# Unload first if already loaded
|
||
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
||
launchctl load -w "$PLIST_FILE"
|
||
echo "✅ Agent loaded. Script will run every $((INTERVAL / 60)) minutes."
|
||
echo " Logs → $LOG_OUT"
|
||
}
|
||
|
||
cmd_uninstall() {
|
||
echo "🗑 Uninstalling Downloads Auto-Uploader..."
|
||
if [[ -f "$PLIST_FILE" ]]; then
|
||
launchctl unload -w "$PLIST_FILE" 2>/dev/null || true
|
||
rm -f "$PLIST_FILE"
|
||
echo "✅ Removed $PLIST_FILE"
|
||
else
|
||
echo "ℹ️ Plist not found — already uninstalled."
|
||
fi
|
||
if [[ -L "$SYMLINK" ]]; then
|
||
rm -f "$SYMLINK"
|
||
echo "✅ Removed symlink $SYMLINK"
|
||
fi
|
||
}
|
||
|
||
cmd_status() {
|
||
echo "── Status ────────────────────────────────"
|
||
if launchctl list | grep -q "$LABEL"; then
|
||
echo "✅ Agent is RUNNING (label: $LABEL)"
|
||
launchctl list "$LABEL" 2>/dev/null || true
|
||
else
|
||
echo "⏹ Agent is NOT loaded."
|
||
fi
|
||
echo ""
|
||
echo "── Files ─────────────────────────────────"
|
||
echo " Script dir : $SCRIPT_DIR"
|
||
echo " Scan dir : $(dirname "$SCRIPT_DIR")"
|
||
echo " Config : $SCRIPT_DIR/downloads_uploader_config.json"
|
||
echo " State : $SCRIPT_DIR/.uploader_state.json"
|
||
echo " App log : $SCRIPT_DIR/uploader_log.txt"
|
||
echo " Stdout log : $LOG_OUT"
|
||
echo " Stderr log : $LOG_ERR"
|
||
echo ""
|
||
echo "── Recent activity (uploader_log.txt) ────"
|
||
LOG_MAIN="$SCRIPT_DIR/uploader_log.txt"
|
||
[[ -f "$LOG_MAIN" ]] && tail -25 "$LOG_MAIN" || echo "(no log yet)"
|
||
}
|
||
|
||
cmd_run() {
|
||
echo "▶ Running uploader now (one-shot)..."
|
||
"$PYTHON" "$PYTHON_SCRIPT"
|
||
}
|
||
|
||
# ───────────────────────────────────────────────
|
||
case "${1:-help}" in
|
||
install) cmd_install ;;
|
||
uninstall) cmd_uninstall ;;
|
||
status) cmd_status ;;
|
||
run) cmd_run ;;
|
||
*)
|
||
echo "Usage: $0 {install|uninstall|status|run}"
|
||
echo ""
|
||
echo " install Register the launchd agent (runs every 5 min)"
|
||
echo " uninstall Remove the launchd agent"
|
||
echo " status Show if the agent is running + recent log"
|
||
echo " run Run the uploader once right now"
|
||
;;
|
||
esac
|