#!/bin/bash # apm-wo-analysis local drop-folder uploader (optional zero-touch ingestion). # # Mirrors the proven stampli-drop-folder pattern. launchd invokes the INSTALLED # copy at ~/.local/bin/apm-wo-uploader.sh, which must live OUTSIDE ~/Documents: # macOS TCC denies launchd read access to ~/Documents, ~/Desktop, ~/Downloads, # and a repo-path script fails silently with LastExitStatus=32256. Re-copy this # source to ~/.local/bin after editing it. # # Uploads new .xlsx/.csv exports to the raw/ prefix using the scoped `apm-wo-drop` # profile (IAM user apm-wo-drop-uploader — s3:PutObject on raw/ only), then # archives them locally. The classifier Lambda is S3-triggered from raw/. set -euo pipefail DROP_DIR="$HOME/apm-wo-drop" UPLOADED_DIR="$DROP_DIR/uploaded" # Log and lock MUST sit outside WatchPaths (~/apm-wo-drop). Writing them # inside the drop folder retriggers launchd on every log line and lock # mkdir/rmdir, which is what ballooned .upload.log to tens of MB. STATE_DIR="$HOME/.local/log" LOG_FILE="$STATE_DIR/apm-wo-uploader.log" LOCK_DIR="$STATE_DIR/apm-wo-uploader.lock" BUCKET="apm-wo-analysis-exports-011934824531" PROFILE="${APM_WO_AWS_PROFILE:-apm-wo-drop}" mkdir -p "$UPLOADED_DIR" "$STATE_DIR" exec >> "$LOG_FILE" 2>&1 # Single-flight: WatchPaths can fire several times for one save. if ! mkdir "$LOCK_DIR" 2>/dev/null; then echo "$(date '+%Y-%m-%dT%H:%M:%S') skipping — another run holds the lock" exit 0 fi trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT # Let the file finish writing before uploading. sleep 2 shopt -s nullglob uploaded_count=0 failed_count=0 for f in "$DROP_DIR"/*.xlsx "$DROP_DIR"/*.csv; do [ -f "$f" ] || continue name=$(basename "$f") ts=$(date '+%Y%m%d-%H%M%S') echo "$(date '+%Y-%m-%dT%H:%M:%S') uploading $name" if aws --profile "$PROFILE" s3 cp "$f" "s3://$BUCKET/raw/$name"; then mv "$f" "$UPLOADED_DIR/$ts-$name" echo "$(date '+%Y-%m-%dT%H:%M:%S') OK -> uploaded/$ts-$name" uploaded_count=$((uploaded_count + 1)) osascript -e "display notification \"Uploaded $name\" with title \"APM WO Uploader\"" 2>/dev/null || true else echo "$(date '+%Y-%m-%dT%H:%M:%S') FAIL $name" failed_count=$((failed_count + 1)) osascript -e "display notification \"Failed to upload $name — see $LOG_FILE\" with title \"APM WO Uploader\" sound name \"Basso\"" 2>/dev/null || true fi done if [ $uploaded_count -eq 0 ] && [ $failed_count -eq 0 ]; then echo "$(date '+%Y-%m-%dT%H:%M:%S') folder change triggered but no .xlsx/.csv files found" fi