# [メタ情報] # 識別子: DAS棚田処理_定期バックアップ_exe # 補足: # [/メタ情報] 要約: このシステムは、macOSのLaunchAgentとPythonスクリプトを組み合わせ、Googleスプレッドシートで管理された設定に基づき、指定されたフォルダー間のファイル同期(ミラーリング)を自動実行するものです。 Pythonスクリプトは、環境変数から認証情報とスプレッドシートIDを取得し、スプレッドシートの「mirror」シートから同期元パス、同期先パス、同期方法(削除オプションの有無など)を読み取ります。その後、`rsync`コマンドを使用して、`.Trashes`や`.DS_Store`などの特定のシステムファイルを除外しつつ、実際のファイル同期を実行します。同期の開始・終了時刻は、スプレッドシートの該当セルに自動で記録されます。 LaunchAgentのplistファイルは、このPythonスクリプトを毎日午前3時30分に自動実行するようスケジュールしています。スクリプト実行に必要なGoogle API認証情報とスプレッドシートIDは、環境変数として渡されます。スクリプトの標準出力とエラー出力は、それぞれ指定されたログファイルにリダイレクトされるため、実行状況や問題の確認が可能です。これにより、定期的なデータバックアップや同期作業が自動化されます。 /Users/XXXXXX/python_scripts/tanada_mirror_sync.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import subprocess import datetime import shutil import gspread from oauth2client.service_account import ServiceAccountCredentials 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(): CREDENTIAL_FILE = os.environ.get("DAS_CRED_JSON", "").strip() SPREADSHEET_ID = os.environ.get("DAS_SHEET_ID", "").strip() if not CREDENTIAL_FILE or not SPREADSHEET_ID: sys.exit(1) SHEET_NAME = 'mirror' scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIAL_FILE, 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 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) src_sync = src_path if src_path.endswith('/') else src_path + '/' dest_sync = dest_path if dest_path.endswith('/') else dest_path + '/' # 削除オプションを最優先にする rsync_cmd = ['/usr/bin/rsync', '-auvd', '--delete', '--delete-excluded', '--ignore-errors'] # 除外設定をさらに厳格に(根本からのパスとして指定) excludes = ['/.Trashes', '/.DocumentRevisions-V100', '/.TemporaryItems', '/.Spotlight-V100', '/.fseventsd', '.DS_Store'] for ex in excludes: rsync_cmd.extend(['--exclude', ex]) 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) end_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") sheet.update_cell(i, 11, end_str) print(f"バックアップ完了: {end_str}") except Exception as e: print(f"[エラー] {e}") if __name__ == "__main__": main() ``` /Users/XXXXXX/Library/LaunchAgents/com.XXXXXX.tanada_mirror.plist ``` Label com.XXXXXX.tanada_mirror ProgramArguments /bin/bash -c env DAS_CRED_JSON="/Users/XXXXXX/keys/service.json" DAS_SHEET_ID="1i6fBlPSORhyCbwv6a7K1x4Jk3rsoKrLkJlGZDGWjj5c" /usr/bin/python3 /Users/XXXXXX/python_scripts/tanada_mirror_sync.py StartCalendarInterval Hour 3 Minute 30 StandardErrorPath /Users/XXXXXX/Library/Logs/tanada_mirror_error.log StandardOutPath /Users/XXXXXX/Library/Logs/tanada_mirror_output.log ```