Last active
September 25, 2025 03:04
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
실행파일 만들기
실행파일 권한 주기
에이전트 재시작 & 즉시 테스트