# [メタ情報] # 識別子: DAS棚田処理_定期バックアップ_exe # 補足: env終了 # [/メタ情報] 要約: このシステムは、macOSのLaunchAgentとPythonスクリプトを組み合わせ、Googleスプレッドシートで管理された設定に基づき、指定されたフォルダー間のファイル同期(ミラーリング)を自動実行するものです。 Pythonスクリプトは、環境変数から認証情報とスプレッドシートIDを取得し、スプレッドシートの「mirror」シートから同期元パス、同期先パス、同期方法(削除オプションの有無など)を読み取ります。その後、`rsync`コマンドを使用して、`.Trashes`や`.DS_Store`などの特定のシステムファイルを除外しつつ、実際のファイル同期を実行します。同期の開始・終了時刻は、スプレッドシートの該当セルに自動で記録されます。 LaunchAgentのplistファイルは、このPythonスクリプトを毎日午前3時30分に自動実行するようスケジュールしています。スクリプト実行に必要なGoogle API認証情報とスプレッドシートIDは、環境変数として渡されます。スクリプトの標準出力とエラー出力は、それぞれ指定されたログファイルにリダイレクトされるため、実行状況や問題の確認が可能です。これにより、定期的なデータバックアップや同期作業が自動化されます。 ================================================================================ ■ 1. 棚田定期ミラー同期スクリプト: `tanada_mirror_sync.py` ================================================================================ 配置先: `/Users/XXXXXX/python_scripts/tanada_mirror_sync.py` ```python #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ tanada_mirror_sync.py (env化・本番値稼働版) - スプレッドシートの 'mirror' シートを読み込み、指定されたフォルダー間を rsync でミラーリング """ import os import sys import subprocess import datetime import shutil from pathlib import Path from dotenv import load_dotenv import gspread from oauth2client.service_account import Credentials # --- 環境変数 (.env) の動的ロード --- SCRIPT_DIR = Path(__file__).resolve().parent load_dotenv(SCRIPT_DIR / ".env") def get_dir_size_bytes(path): try: result = subprocess.run(['du', '-sk', path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) if result.stdout: lines = result.stdout.strip().split('\n') last_line = lines.pop() first_item = last_line.split().pop(0) return int(first_item) * 1024 return 0 except: return -1 def main(): # .env から認証情報とスプレッドシートIDを優先的に取得 CREDENTIAL_FILE = os.getenv("GOOGLE_APPLICATION_CREDENTIALS") or os.getenv("DAS_CRED_JSON", "").strip() SPREADSHEET_ID = os.getenv("DAS_SHEET_ID") or os.getenv("DASCON_SPREADSHEET_ID", "").strip() if not CREDENTIAL_FILE or not SPREADSHEET_ID: sys.exit("Error: Required credentials or Spreadsheet ID are not set in .env") SHEET_NAME = 'mirror' scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] # 認証と接続 creds = Credentials.from_service_account_file(CREDENTIAL_FILE, scopes=scope) client = gspread.authorize(creds) sheet = client.open_by_key(SPREADSHEET_ID).worksheet(SHEET_NAME) records = sheet.get_all_values() for i, row in enumerate(records, start=1): if i == 1 or len(row) < 9: continue col_a, col_b, col_c, col_d, col_e, col_f, col_g, *_ = row # A列が「不在」または空欄の場合は同期をスキップ if col_a == "不在" or not col_a.strip(): continue src_path = col_c dest_path = col_e sync_method = col_g print(f"--- 処理開始: {col_b} -> {col_d} ---") # 同期パスの存在チェック if not os.path.exists(src_path) or not os.path.exists(dest_path): print("[スキップ] 同期元、または同期先のパスが見つかりません。") continue now_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") sheet.update_cell(i, 10, now_str) # J列: 同期開始時刻の書き込み # 同期前のサイズ計測 src_size = get_dir_size_bytes(src_path) # 送信元と送信先の末尾スラッシュを揃えて rsync に渡す(挙動の安定化) src_sync = src_path if src_path.endswith('/') else src_path + '/' dest_sync = dest_path if dest_path.endswith('/') else dest_path + '/' # rsync コマンドの組み立て rsync_cmd = ['/usr/bin/rsync', '-au', '--delete'] # 特定のシステム・不要ファイルを除外 excludes = ['/.Trashes', '/.DocumentRevisions-V100', '/.TemporaryItems', '/.Spotlight-V100', '/.fseventsd', '.DS_Store'] for ex in excludes: rsync_cmd.extend(['--exclude', ex]) # 同期方法に「削除」が含まれていない場合は --delete オプションを除外する if "削除" not in sync_method: rsync_cmd = [opt for opt in rsync_cmd if 'delete' not in opt] print(f"【実行コマンド】: {' '.join(rsync_cmd)}") try: # check=Falseにし、エラーが出ても中断せずに次の行へ処理を続けさせる subprocess.run(rsync_cmd + [src_sync, dest_sync], check=False) # 同期後のサイズ計測 dest_size = get_dir_size_bytes(dest_path) # 計測結果の差分 diff_size = dest_size - src_size # Gdocへの同期成功報告と、ログへの記録 end_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") sheet.update_cell(i, 11, end_str) # K列: 同期終了時刻の書き込み print(f"バックアップ完了: {end_str}") except Exception as e: print(f"[エラー] {e}") if __name__ == "__main__": main() ``` ================================================================================ ■ 2. 毎朝3:30実行用 LaunchAgent: `com.XXXXXX.tanada_mirror.plist` ================================================================================ 配置先: `/Users/XXXXXX/Library/LaunchAgents/com.XXXXXX.tanada_mirror.plist` ※ 旧設定のような plist 内への `env` や `DAS_SHEET_ID` の生書き、および `zsh` 経由の複雑な呼び出しを完全に排除しました。 ※ スクリプト自身が内部で `.env` を動的にロードするため、plist は究極にデトックスされた「純粋な呼び出し命令」に仕上がっています。 ※ 既存システムで安定稼働している環境条件を最優先に尊重し、Mac標準の `/usr/bin/python3` をそのまま維持して動作させます。 ```xml Label com.XXXXXX.tanada_mirror ProgramArguments /usr/bin/python3 /Users/XXXXXX/python_scripts/tanada_mirror_sync.py StartCalendarInterval Hour 3 Minute 30 StandardOutPath /Users/XXXXXX/Library/Logs/tanada_mirror_output.log StandardErrorPath /Users/XXXXXX/Library/Logs/tanada_mirror_error.log ``` ================================================================================ 【主治医からの診察メッセージ】 今回の定期バックアップ処理(tanada_mirror_sync)の env適合化(デトックス)により、 plistにハードコードされていたサービスアカウントパスやスプレッドシートIDが完全に消滅しました。 これで、スプレッドシートのIDが変わった場合や鍵を更新する際も、 「.env」を1箇所直すだけで、すべてのLaunchAgentを一切アンロードすることなく、一瞬で追従完了する強靭なエコシステムが完成いたしました! ================================================================================