# [メタ情報]
# 識別子: メディアおよびJSONをXserverへ更新および自動復帰_exe
# システム名: 未分類
# 技術種別: Misc
# 機能名: Misc
# 使用言語: Shell Script
# 状態: 公開用
# [/メタ情報]
要約:
M1 MacからXserverへのDropboxデータ同期システムは、主に以下のスクリプトとLaunchAgentで構成されています。
`sync_dropbox_xserver_m1.sh`は、Dropbox内のpmedia、mmedia、メタデータをrsyncでXserverの指定ディレクトリへ60秒間隔で自動同期します。このスクリプトは、必要な環境変数の厳格なチェック、SSHキーの検証、Homebrew版rsyncの優先使用、空JSONファイルの同期スキップなど、安全性を重視した設計です。
システムの健全性を維持するため、`sync_xserver_selfheal.sh`が自動復旧機能を提供します。このスクリプトは毎日3:30に実行され、残留しているrsyncプロセスを強制終了し、Dropboxデスクトップアプリのフリーズ(ログ更新が15分以上停止)を検知するとアプリを再起動します。その後、メインの同期LaunchAgentをリロードし、タスクを再始動させます。
さらに、selfhealスクリプトがDropboxを再起動した場合、毎日4:00に管理者へメールで通知するレポート機能も備えています。
全てのスクリプトはmacOSのLaunchAgentによって自動実行され、`.env`ファイルから環境変数を安全にロードし、不足している場合は実行を停止するなど、堅牢な運用がなされています。
<任意のフォルダパス>/sync_dropbox_xserver_m1.sh
```
#!/bin/bash
# ===== LaunchAgent 用:PATH を明示的に設定 =====
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export LANG=en_US.UTF-8
# ===== 1. 金庫(.env)のロードとMeisterの環境差を自動吸収 =====
if [ -f "$HOME/python_scripts/.env" ]; then
export $(grep -v '^#' "$HOME/python_scripts/.env" | xargs)
fi
# ===== 2. 鉄壁の自律停止ガード(安全な起動時チェック) =====
REQUIRED_VARS=("LOCAL_DROPBOX_BASE" "LOCAL_META_DIR" "XS_USER" "XS_HOST" "XS_SSH_PORT" "SSH_KEY_PATH" "XSERVER_PMEDIA_DIR" "XSERVER_MMEDIA_DIR" "XSERVER_DEST_DIR")
MISSING_VARS=()
for var in "${REQUIRED_VARS[@]}"; do
if [ -z "${!var}" ]; then
MISSING_VARS+=("$var")
fi
done
if [ ${#MISSING_VARS[@]} -ne 0 ]; then
echo "❌ [ERROR] 必要な環境変数が .env に設定されていません: ${MISSING_VARS[*]}" >> /tmp/sync_dropbox_trace.log
exit 1
fi
# SSHキーパスの「~」記号を絶対パスへ強制展開
SSH_KEY_PATH="${SSH_KEY_PATH/#\~/$HOME}"
if [ ! -f "$SSH_KEY_PATH" ]; then
echo "❌ [ERROR] SSHキーファイルが物理的に存在しません: $SSH_KEY_PATH" >> /tmp/sync_dropbox_trace.log
exit 1
fi
# ===== rsync: Homebrew 版を優先的に特定 =====
if [ -x /opt/homebrew/bin/rsync ]; then
RSYNC="/opt/homebrew/bin/rsync"
else
RSYNC="/usr/bin/rsync"
fi
# ===== ログディレクトリ =====
LOG_DIR="$HOME/scripts/logs"
mkdir -p "$LOG_DIR"
# ---- /tmp にトレース(launchd 実行確認) ----
TS="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$TS] sync_dropbox_xserver_m1.sh START (pid=$$, user=$(id -un))" >> /tmp/sync_dropbox_trace.log
echo "[$TS] ==== sync_dropbox_xserver_m1.sh START ====" >> "$LOG_DIR/sync_dropbox_debug.log"
# ---------------------------------------------------------
# 1) pmedia → Xserver: wp-content/pmedia
# ---------------------------------------------------------
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync pmedia START" >> "$LOG_DIR/sync_dropbox_debug.log"
"$RSYNC" -av --iconv=UTF-8-MAC,UTF-8 \
--delete --perms --chmod=Da+rx,Fa+r \
-e "ssh -i $SSH_KEY_PATH -p $XS_SSH_PORT -o StrictHostKeyChecking=accept-new" \
--exclude='@esDir' \
"$LOCAL_DROPBOX_BASE/pmedia/" \
"$XS_USER@$XS_HOST:$XSERVER_PMEDIA_DIR/" \
>> "$LOG_DIR/rsync_pmedia.log" 2>>"$LOG_DIR/rsync_pmedia_error.log"
P_RET=$?
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync pmedia END (exit=$P_RET)" >> "$LOG_DIR/sync_dropbox_debug.log"
sleep 1
# ---------------------------------------------------------
# 2) mmedia → Xserver: wp-content/mmedia
# ---------------------------------------------------------
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync mmedia START" >> "$LOG_DIR/sync_dropbox_debug.log"
"$RSYNC" -av --iconv=UTF-8-MAC,UTF-8 \
--delete --perms --chmod=Da+rx,Fa+r \
-e "ssh -i $SSH_KEY_PATH -p $XS_SSH_PORT -o StrictHostKeyChecking=accept-new" \
--exclude='@esDir' \
"$LOCAL_DROPBOX_BASE/mmedia/" \
"$XS_USER@$XS_HOST:$XSERVER_MMEDIA_DIR/" \
>> "$LOG_DIR/rsync_mmedia.log" 2>>"$LOG_DIR/rsync_mmedia_error.log"
M_RET=$?
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync mmedia END (exit=$M_RET)" >> "$LOG_DIR/sync_dropbox_debug.log"
sleep 1
# ---------------------------------------------------------
# 3) meta_gd_wp_data → Xserver: _secure/wp_json
# ---------------------------------------------------------
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync meta_gd_wp_data START" >> "$LOG_DIR/sync_dropbox_debug.log"
# ★ 修正:JSONが空(0バイト)の場合は同期をスキップする安全装置
if [ ! -s "$LOCAL_META_DIR/dropbox_wp_library.json" ]; then
echo "⚠️ JSONファイルが空(0バイト)のため、転送をスキップします。" >> "$LOG_DIR/sync_dropbox_debug.log"
else
"$RSYNC" -av \
--delete --perms --chmod=Da+rx,Fa+r \
-e "ssh -i $SSH_KEY_PATH -p $XS_SSH_PORT -o StrictHostKeyChecking=accept-new" \
"$LOCAL_META_DIR/" \
"$XS_USER@$XS_HOST:$XSERVER_DEST_DIR/" \
>> "$LOG_DIR/rsync_meta_gd_wp_data.log" 2>>"$LOG_DIR/rsync_meta_gd_wp_data_error.log"
fi
J_RET=$?
echo "[$(date '+%Y-%m-%d %H:%M:%S')] rsync meta_gd_wp_data END (exit=$J_RET)" >> "$LOG_DIR/sync_dropbox_debug.log"
exit 0
```
<任意のフォルダパス>/com.XXXXXX.sync_dropbox_xserver_m1.plist
```
Label
com.XXXXXX.sync_dropbox_xserver_m1
ProgramArguments
/bin/bash
<任意のフォルダパス>/sync_dropbox_xserver_m1.sh
RunAtLoad
StartInterval
60
StandardOutPath
/tmp/sync_dropbox_xserver_m1.out
StandardErrorPath
/tmp/sync_dropbox_xserver_m1.err
```
上と合わせて、毎朝3:30にrsyncの残留をチェックして、
残っていればkill unload loadする。
<任意のフォルダパス>/sync_xserver_selfheal.sh
```
#!/bin/bash
# pmedia / mmedia 同期の自動復旧スクリプト
# - pmedia / mmedia 関連の rsync が残っていれば kill
# - Dropbox デスクトップの簡易ヘルスチェック(ログ更新停止なら再起動)
# - メイン同期 LaunchAgent(com.XXXXXX.sync_dropbox_xserver_m1)を reload
# - Dropboxを「このスクリプトが」再起動した場合のみ、日次(4:00)で1回メール通知
# ※sync_dropbox_xserver_m1.sh 本体は変更しない
set -u
# ===== 1. 金庫(.env)のロードとMeisterの環境差を自動吸収 =====
if [ -f "$HOME/python_scripts/.env" ]; then
export $(grep -v '^#' "$HOME/python_scripts/.env" | xargs)
fi
# ===== 2. 鉄壁の自律停止ガード(安全な起動時チェック) =====
REQUIRED_VARS=("MAIL_TO" "XS_USER" "XS_HOST")
MISSING_VARS=()
for var in "${REQUIRED_VARS[@]}"; do
if [ -z "${!var}" ]; then
MISSING_VARS+=("$var")
fi
done
if [ ${#MISSING_VARS[@]} -ne 0 ]; then
echo "❌ [ERROR] 必要な環境変数が .env に設定されていません: ${MISSING_VARS[*]}"
exit 1
fi
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export LANG=en_US.UTF-8
# ========= 設定 =========
LOG_DIR="$HOME/scripts/logs"
SELFHEAL_LOG="$LOG_DIR/sync_xserver_selfheal.log"
PLIST="$HOME/Library/LaunchAgents/com.XXXXXX.sync_dropbox_xserver_m1.plist"
# Dropboxログ(存在する範囲で一番新しいものを拾う)
DROPBOX_LOG_DIR="$HOME/Library/Logs/Dropbox"
DROPBOX_STALE_SEC=$((15 * 60)) # 15分以上ログ更新が止まっていたら怪しい
DROPBOX_WAIT_AFTER_RESTART=20 # 再起動後に少し待つ
# メール通知(金庫 .env から読み込み済。件名プレフィックスは環境維持)
MAIL_SUBJECT_PREFIX="[XXXXXX] Dropbox selfheal report"
STATE_RESTART_LOG="$LOG_DIR/dropbox_restart_events.log" # 再起動イベントを追記(時刻が残る)
STATE_LAST_REPORT="$LOG_DIR/dropbox_restart_last_report.ts" # 最後に通知した時刻(epoch)
# ログディレクトリ作成(既存の sync_dropbox_xserver_m1.sh と同じ場所)
mkdir -p "$LOG_DIR"
# ========= ログ出力関数 =========
log() {
local ts
ts="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$ts] $1"
}
# ========= Dropboxの稼働状態チェック =========
dropbox_running() {
pgrep -x "Dropbox" >/dev/null 2>&1
}
# ========= 最新Dropboxログファイルの特定 =========
latest_dropbox_log() {
local l_file
l_file=$(ls -t "$DROPBOX_LOG_DIR"/*.log 2>/dev/null | head -n 1)
echo "${l_file:-}"
}
# ========= メール送信処理(Mail.app連携、タイムアウト設定) =========
send_mail_via_mailapp() {
local subj body
subj="$1"
body="$2"
log "📧 Mail.app 経由でメール送信を試みます... (to: $MAIL_TO)"
/usr/bin/osascript <> "$STATE_RESTART_LOG"
}
restart_dropbox() {
log "🟠 Dropbox 再起動を試行します"
/usr/bin/osascript -e 'quit app "Dropbox"' >/dev/null 2>&1 || true
sleep 3
pkill -x "Dropbox" >/dev/null 2>&1 || true
sleep 2
open -a "Dropbox" || log "⚠ Dropbox.app を open できませんでした"
log "🟡 Dropbox 起動後の待機: ${DROPBOX_WAIT_AFTER_RESTART}s"
sleep "$DROPBOX_WAIT_AFTER_RESTART"
# 「このスクリプトが再起動した」記録
record_dropbox_restart_event
}
# ========= Dropboxヘルスチェック(再起動したかどうか返す) =========
# return:
# 0 = OK(再起動なし)
# 10 = 再起動した
dropbox_healthcheck() {
log "🔎 Dropbox ヘルスチェック開始"
if ! dropbox_running; then
log "⚠ Dropbox が起動していないようです"
restart_dropbox
return 10
fi
local lf
lf="$(latest_dropbox_log)"
if [[ -z "${lf:-}" || ! -f "$lf" ]]; then
log "ℹ Dropbox ログが見つかりません($DROPBOX_LOG_DIR)。検出精度は落ちます。"
return 0
fi
local now mt age
now=$(date +%s)
mt=$(stat -f %m "$lf" 2>/dev/null || echo 0)
age=$((now - mt))
log "ℹ 最新Dropboxログ: $lf(更新から ${age}s)"
if (( age > DROPBOX_STALE_SEC )); then
log "⚠ Dropbox ログ更新が ${DROPBOX_STALE_SEC}s 以上止まっています → フリーズ疑い"
restart_dropbox
# 再起動後にもう一度チェック(参考)
lf="$(latest_dropbox_log)"
if [[ -n "${lf:-}" && -f "$lf" ]]; then
mt=$(stat -f %m "$lf" 2>/dev/null || echo 0)
now=$(date +%s)
age=$((now - mt))
log "✅ 再起動後ログ更新チェック: 更新から ${age}s"
fi
return 10
else
log "✅ Dropbox は概ね正常(ログ更新が動いています)"
return 0
fi
}
# ========= 日次レポート(4:00に呼ぶ想定) =========
# - 前回通知以降に「このスクリプトが再起動した記録」があれば1通だけメール
# - なければ送らない
daily_report() {
log "📧 日次レポート開始(再起動があった日だけ送信)"
local now last_ts
now=$(date +%s)
if [[ -f "$STATE_LAST_REPORT" ]]; then
last_ts=$(cat "$STATE_LAST_REPORT")
else
last_ts=$((now - 30 * 3600)) # なければ約30時間前
fi
# スナップショット時刻を書き込み
echo "$now" > "$STATE_LAST_REPORT"
if [[ ! -f "$STATE_RESTART_LOG" ]]; then
log "✅ 再起動履歴ファイルが存在しません -> 終了"
return 0
fi
# 履歴ファイルから「前回の通知以降に起きた再起動」をフィルタ
local filter_since events
filter_since=$(date -r "$last_ts" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -jf '%s' "$last_ts" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "")
log "🔎 フィルタ基準時刻: $filter_since"
events=$(
while read -r line; do
if [[ "$line" =~ ^\[([0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2})\] ]]; then
local dt_str="${BASH_REMATCH[1]}"
if [[ "$dt_str" >= "$filter_since" ]]; then
echo "$line"
fi
fi
done < "$STATE_RESTART_LOG"
)
if [[ -z "${events// /}" ]]; then
log "✅ 直近30時間以内の再起動イベントはありません -> 終了"
return 0
fi
local subject body
subject="${MAIL_SUBJECT_PREFIX} - restart detected"
body=$(
cat <> "$SELFHEAL_LOG"
fi
}
# ========= メイン:引数で動作切替 =========
MODE="${1:-selfheal}"
if [[ "$MODE" == "report" ]]; then
{
daily_report
} >>"$SELFHEAL_LOG" 2>&1
exit 0
fi
# ===== 通常 self-heal =====
{
log "===== self-heal START ====="
# ★ 0) Dropbox の健全性チェック(先にやる:クラウド→ローカル反映を期待)
dropbox_healthcheck || true
# 1) pmedia / mmedia 関連の rsync プロセスを探して kill
PIDS_P=$(pgrep -f "rsync .*pmedia" || true)
PIDS_M=$(pgrep -f "rsync .*mmedia" || true)
RSYNC_PIDS=""
if [[ -n "$PIDS_P" ]]; then
RSYNC_PIDS="$RSYNC_PIDS $PIDS_P"
fi
if [[ -n "$PIDS_M" ]]; then
RSYNC_PIDS="$RSYNC_PIDS $PIDS_M"
fi
if [[ -n "${RSYNC_PIDS// /}" ]]; then
log "🔍 残留 rsync プロセス検出: $RSYNC_PIDS"
for pid in $RSYNC_PIDS; do
log "➡ kill $pid"
kill "$pid" 2>/dev/null || log "⚠ kill 失敗: $pid"
done
sleep 2
else
log "✅ pmedia/mmedia 関連 rsync 残留なし"
fi
# 2) メイン同期 LaunchAgent の reload
if [[ -f "$PLIST" ]]; then
log "🔁 LaunchAgent reload: $PLIST"
launchctl unload "$PLIST" 2>/dev/null || log "ℹ unload でエラー (無視可)"
launchctl load "$PLIST" || log "⚠ launchctl load でエラー"
else
log "⚠ メイン同期 plist が見つかりません: $PLIST"
fi
log "===== self-heal END ====="
} >>"$SELFHEAL_LOG" 2>&1
```
<任意のフォルダパス>/com.XXXXXX.sync_xserver_selfheal.plist
```
Label
com.XXXXXX.sync_xserver_selfheal
ProgramArguments
/bin/bash
<任意のフォルダパス>/sync_xserver_selfheal.sh
StartCalendarInterval
Hour
3
Minute
30
StandardOutPath
/tmp/sync_xserver_selfheal.out
StandardErrorPath
/tmp/sync_xserver_selfheal.err
```
<任意のフォルダパス>/com.XXXXXX.sync_xserver_selfheal_report.plist
```
Label
com.XXXXXX.sync_xserver_selfheal_report
ProgramArguments
/bin/bash
<任意のフォルダパス>/sync_xserver_selfheal.sh
report
StartCalendarInterval
Hour
4
Minute
0
StandardOutPath
/tmp/sync_xserver_selfheal_report.out
StandardErrorPath
/tmp/sync_xserver_selfheal_report.err
```