# [メタ情報]# 識別子: dropboxリンクの定期的自動修復_exe# システム名: 未分類# 技術種別: Misc# 機能名: Misc# 使用言語: py plist# 状態: 実行用# [/メタ情報]要約: このシステムはmacOS上でDropbox共有リンクの自動巡回・修復、およびリンクの有効性チェックを行うものです。`fix_dropbox_links.py`はスプレッドシートのローカルパスを基に、共有リンクをダウンロード可能な形式(raw=1)に変換し、Mac特有の濁点問題を修正してAPI経由で最新リンクを取得します。差分があればスプレッドシートを更新しフラグを立てます。この処理は`com.XXXXXX.fix_dropbox_links.plist`により1時間ごとに自動実行されます。さらに今回追加された`check_dropbox_link.py`は、任意のDropbox共有リンクが有効か(リンク切れしていないか)をAPIを用いて直接確認し、結果をログに出力します。これらのスクリプト群により、Dropbox共有リンクの継続的な整合性維持と異常検知を自動化、手動介入を減らした堅牢な運用を実現しています。 実行状態の確認ログ tail -n 20 ~/Library/Logs/fix_dropbox_links.log /Users/XXXXXX/python_scripts/fix_dropbox_links.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ fix_dropbox_links.py (改良版) ・Mac特有の濁点問題(NFC/NFD)を自動修正してAPIへ渡す ・ターミナル画面にもログを直接表示して動作を見える化 """ import os import re import time import logging import unicodedata import dropbox import gspread from google.oauth2.service_account import Credentials from dotenv import load_dotenv # .envファイルのロード env_path = "/Users/XXXXXX/python_scripts/.env" load_dotenv(env_path) # ====== 設定 ====== APP_KEY = os.environ.get("APP_KEY") APP_SECRET = os.environ.get("APP_SECRET") REFRESH_TOKEN = os.environ.get("REFRESH_TOKEN") SA_JSON_PATH = os.environ.get("SA_JSON_PATH") SPREADSHEET_ID = os.environ.get("F1_SPREADSHEET_ID") SHEET_NAME = "sheet1" LOG_PATH = os.path.expanduser("~/Library/Logs/fix_dropbox_links.log") # ====== ロギング設定(画面にも出力するよう改良) ====== os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler(LOG_PATH, encoding="utf-8"), logging.StreamHandler() # ターミナルにも表示 ] ) def get_dbx(): return dropbox.Dropbox( oauth2_refresh_token=REFRESH_TOKEN, app_key=APP_KEY, app_secret=APP_SECRET ) def get_dropbox_path(local_path): possible_roots = [ "/Users/XXXXXX/Dropbox/dropbox_1", "/Users/XXXXXX/Dropbox/dropbox_1", # ← XXXXXX を追加 "/Volumes/NO3_SSD/Dropbox/dropbox_1" ] for root in possible_roots: if local_path.startswith(root): # Mac特有の濁点(NFD)をDropboxが認識できる(NFC)に変換 rel_path = "/dropbox_1" + local_path[len(root):] return unicodedata.normalize('NFC', rel_path) return None def convert_to_raw_url(url): url = re.sub(r'dl=0', 'raw=1', url) if 'raw=1' not in url: sep = '&' if '?' in url else '?' url += f"{sep}raw=1" return url def get_current_shared_link(dbx, dbx_path): try: links = dbx.sharing_list_shared_links(path=dbx_path, direct_only=True).links if links: return convert_to_raw_url(links[0].url) except Exception as e: pass try: link = dbx.sharing_create_shared_link_with_settings(dbx_path) return convert_to_raw_url(link.url) except Exception as e: logging.error(f"リンク作成エラー {dbx_path}: {e}") return None def main(): logging.info("=== リンク巡回・修復スクリプト 開始 ===") try: dbx = get_dbx() creds = Credentials.from_service_account_file(SA_JSON_PATH, scopes=["https://www.googleapis.com/auth/spreadsheets"]) gc = gspread.authorize(creds) ws = gc.open_by_key(SPREADSHEET_ID).worksheet(SHEET_NAME) rows = ws.get_all_values() except Exception as e: logging.error(f"接続エラー: {e}") return if len(rows) <= 1: logging.info("データがありません。") return updates = [] COL_A_LINK = 0 COL_F_PATH = 5 api_calls = 0 for i, row in enumerate(rows[1:], start=2): if len(row) <= COL_F_PATH: continue current_link = row[COL_A_LINK].strip() local_path = row[COL_F_PATH].strip() if not local_path or ("/mmedia/" not in local_path and "/pmedia/" not in local_path): continue dbx_path = get_dropbox_path(local_path) if not dbx_path: continue latest_link = get_current_shared_link(dbx, dbx_path) api_calls += 1 if latest_link and current_link != latest_link: logging.info(f"✨ リンク変更検出 [行{i}]: {local_path}") updates.append({"range": f"A{i}:A{i}", "values": [[latest_link]]}) updates.append({"range": f"K{i}:K{i}", "values": [["CHG"]]}) # ADD2の処理と混線しないよう一旦CHGにします time.sleep(0.3) if api_calls % 100 == 0: logging.info(f"{api_calls}件処理完了...") time.sleep(2) if updates: try: ws.batch_update(updates) logging.info(f"✅ {len(updates) // 2} 件のリンクを修復し、更新フラグを立てました。") except Exception as e: logging.error(f"スプレッドシート一括更新エラー: {e}") else: logging.info("修復が必要なリンクはありませんでした。") logging.info("=== リンク巡回・修復スクリプト 終了 ===") if __name__ == "__main__": main() ``` === Python(check_dropbox_link.py) === /Users/XXXXXX/python_scripts/check_dropbox_link.py ```python import sys import logging import dropbox import os from dropbox.exceptions import ApiError from dotenv import load_dotenv # .envファイルのロード env_path = "/Users/XXXXXX/python_scripts/.env" load_dotenv(env_path) LOG_PATH = os.path.expanduser("~/Library/Logs/check_dropbox_link.log") os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True) logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.StreamHandler(), logging.FileHandler(LOG_PATH, encoding="utf-8") ] ) APP_KEY = os.environ.get("APP_KEY") APP_SECRET = os.environ.get("APP_SECRET") REFRESH_TOKEN = os.environ.get("REFRESH_TOKEN") def get_dbx(): return dropbox.Dropbox( oauth2_refresh_token=REFRESH_TOKEN, app_key=APP_KEY, app_secret=APP_SECRET ) def check_dropbox_link_api(url: str): dbx = get_dbx() logging.info(f"リンクを確認中: {url}") try: metadata = dbx.sharing_get_shared_link_metadata(url) logging.info(f"✅ 有効なリンクです: {metadata.name}") return True except ApiError as e: if e.error.is_shared_link_not_found(): logging.error(f"❌ 無効なリンクです (Not Found)") else: logging.error(f"❌ APIエラー: {e}") return False except Exception as e: logging.error(f"❌ 予期せぬエラー: {e}") return False if __name__ == "__main__": test_urls = [] if len(sys.argv) > 1: test_urls = sys.argv[1:] else: logging.info("URLが指定されていません。引数にリンクを渡してください。") for u in test_urls: check_dropbox_link_api(u) ``` /Users/XXXXXX/Library/LaunchAgents/com.XXXXXX.fix_dropbox_links.plist ``` Label com.XXXXXX.fix_dropbox_links ProgramArguments /usr/bin/python3 /Users/XXXXXX/python_scripts/fix_dropbox_links.py StartCalendarInterval Minute 0 StandardOutPath /Users/XXXXXX/Library/Logs/fix_dropbox_links_out.log StandardErrorPath /Users/XXXXXX/Library/Logs/fix_dropbox_links_err.log ```