Skip to content

Instantly share code, notes, and snippets.

@kimjj81
Last active September 25, 2025 03:04
Show Gist options
  • Save kimjj81/9eb50a197a0857f58019e085d3104398 to your computer and use it in GitHub Desktop.
Save kimjj81/9eb50a197a0857f58019e085d3104398 to your computer and use it in GitHub Desktop.
Auto backup from SD card in mac. Change VOLUME_NAME. rsync 2.6.9
mkdir -p "$HOME/Library/LaunchAgents"
tee "$HOME/Library/LaunchAgents/com.sd.autobackup.plist" >/dev/null <<'EOF'
<?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>com.sd.autobackup</string>
<key>WatchPaths</key>
<array>
<string>/Volumes</string>
</array>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/sd_auto_backup.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<!-- 경로 상태 변화에 반응 -->
<key>KeepAlive</key>
<dict>
<key>PathState</key>
<dict>
<key>SD카드볼륨명</key>
<true/>
<key>저장디스크루트</key>
<true/>
</dict>
</dict>
<key>StandardOutPath</key>
<string>${HOME}/Library/Logs/sd_auto_backup/agent_stdout.log</string>
<key>StandardErrorPath</key>
<string>${HOME}/Library/Logs/sd_auto_backup/agent_stderr.log</string>
</dict>
</plist>
EOF
#!/usr/bin/env bash
# sd_auto_backup.sh
# - 트리거: launchd(WatchPaths=/Volumes)
# - 조건: 소스 SD 볼륨이 "XXX", 목적지 루트 볼륨 "XXXX" 마운트 시 동기화
# - 기능: rsync 동기화, 구버전 rsync 진행률 옵션 폴백, 중복 실행 방지, 로그, 배터리 전원 시 스킵(플래그)
set -euo pipefail
# =========[ 사용자 설정 ]=========
# 소스 SD 카드 볼륨 이름
VOLUME_NAME="YOUR_SOURCE_VOLUME_NAME"
# 백업 대상(외장 디스크 루트 볼륨 이름 + 최종 경로)
DEST_ROOT_VOLUME="YOUR_BACKUP_VOLUME"
DEST_SUB_DIRECTORY="YOUR_BACKUP_DIRECTORY"
DEST_DIR="/Volumes/${DEST_ROOT_VOLUME}/${DEST_SUB_DIRECTORY}"
# 목적지에서 원본에 없는 파일 삭제 동기화 여부(true/false)
ENABLE_DELETE=false
# 배터리 전원일 때 스킵할지 여부(true=스킵, false=스킵 안 함)
SKIP_ON_BATTERY=false
# 제외 패턴 (필요 시 추가)
EXCLUDES=(
".Spotlight-V100"
".Trashes"
".fseventsd"
"System Volume Information"
)
# =================================
SRC="/Volumes/${VOLUME_NAME}"
LOG_DIR="$HOME/Library/Logs/sd_auto_backup"
LOG_FILE="$LOG_DIR/backup_$(date +%Y%m%d).log"
LOCK_FILE="$HOME/Library/Caches/sd_auto_backup_${VOLUME_NAME}.lock"
mkdir -p "$LOG_DIR"
log_msg() {
printf "[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE"
}
# 배터리 전원 감지 (AC/UPS/배터리 모두 고려)
on_battery_power() {
if ! command -v pmset >/dev/null 2>&1; then
# pmset 없으면 판단 불가 → 배터리 아님으로 간주
return 1
fi
# macOS에서는 아래 출력의 첫 줄에 전원 상태가 표시됩니다.
# 예) "Now drawing from 'AC Power'" 또는 "Now drawing from 'Battery Power'"
local line
line="$(pmset -g ps 2>/dev/null | head -n1 || true)"
if echo "$line" | grep -q "Battery Power"; then
return 0
fi
# 일부 환경(UPS) 대비
if echo "$line" | grep -q "UPS Power"; then
return 0
fi
return 1
}
# 중복 실행 방지(최대 120분)
if [ -f "$LOCK_FILE" ]; then
if [ "$(find "$LOCK_FILE" -mmin +120 2>/dev/null | wc -l)" -gt 0 ]; then
log_msg "Stale lock detected. Removing $LOCK_FILE"
rm -f "$LOCK_FILE"
else
log_msg "Backup already running. Exit."
exit 0
fi
fi
trap 'rm -f "$LOCK_FILE"' EXIT
touch "$LOCK_FILE"
# ---- 배터리 전원 스킵 옵션 ----
if [ "${SKIP_ON_BATTERY}" = true ] && on_battery_power; then
log_msg "Battery power detected and SKIP_ON_BATTERY=true. Skipping backup."
exit 0
fi
# ---- 목적지 루트 볼륨(WDBlackDataVolume) 마운트 확인 ----
if ! mount | grep -q "/Volumes/${DEST_ROOT_VOLUME}"; then
log_msg "Destination root volume '${DEST_ROOT_VOLUME}' not mounted. Skipping backup."
exit 0
fi
# ---- 소스 SD 카드 마운트 확인 ----
if ! mount | grep -q "/Volumes/${VOLUME_NAME}"; then
log_msg "Source volume '${VOLUME_NAME}' not mounted. Skipping backup."
exit 0
fi
# 경로 존재 확인/생성
if [ ! -d "$SRC" ]; then
log_msg "Source path not found: $SRC"
exit 1
fi
mkdir -p "$DEST_DIR"
# rsync 바이너리 선택 (Homebrew rsync 우선)
if [ -x "/opt/homebrew/bin/rsync" ]; then
RSYNC_BIN="/opt/homebrew/bin/rsync"
elif [ -x "/usr/local/bin/rsync" ]; then
RSYNC_BIN="/usr/local/bin/rsync"
else
RSYNC_BIN="/usr/bin/rsync"
fi
# rsync 옵션 구성 (버전에 따라 진행 표시 옵션 자동 선택)
RSYNC_OPTS=( -a -v -h )
$ENABLE_DELETE && RSYNC_OPTS+=( --delete )
# ★ 추가: FAT/exFAT 타임스탬프 보정
RSYNC_OPTS+=( --modify-window=2 )
for p in "${EXCLUDES[@]}"; do
RSYNC_OPTS+=( --exclude="$p" )
done
# rsync 3.x 이상이면 --info=progress2, 아니면 --progress
if "$RSYNC_BIN" --version 2>/dev/null | head -n1 | grep -qE 'version ([3-9]|[1-9][0-9])\.'; then
RSYNC_OPTS+=( --info=progress2 --stats )
else
RSYNC_OPTS+=( --progress --stats )
fi
log_msg "Starting backup: '${SRC}/' -> '${DEST_DIR}/' (using $RSYNC_BIN)"
if "$RSYNC_BIN" "${RSYNC_OPTS[@]}" "$SRC/" "$DEST_DIR/" 2>&1 | tee -a "$LOG_FILE"; then
log_msg "Backup finished successfully."
exit 0
else
log_msg "Backup failed."
exit 1
fi
@kimjj81
Copy link
Author

kimjj81 commented Aug 13, 2025

실행파일 만들기

sudo vi /usr/local/bin/sd_auto_backup.sh

실행파일 권한 주기

sudo chmod +x /usr/local/bin/sd_auto_backup.sh

에이전트 재시작 & 즉시 테스트

launchctl unload "$HOME/Library/LaunchAgents/com.sd.autobackup.plist" 2>/dev/null || true
launchctl load "$HOME/Library/LaunchAgents/com.sd.autobackup.plist"

# 외장 디스크와 SD 카드가 둘 다 마운트된 상태에서:
 /usr/local/bin/sd_auto_backup.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment