# [メタ情報] # 識別子: マイライブラリ_dropboxAPI_Mac_GASデータ交換_exe # システム名: マイライブラリ_dropboxAPI_Mac_GASデータ交換 # 技術種別: Misc # 機能名: Misc # 使用言語: Python # 状態: 公開用 # [/メタ情報] 要約: M2 Mac環境で、PythonによるDropbox APIおよびGoogle Apps Script (GAS)連携スクリプト群が紹介されている。`dropbox_auth_setup.py`はOAuth 2.0フローを用いてDropbox APIのアクセストークンとリフレッシュトークンを取得。ブラウザでの認証、ローカルサーバーでの認証コード受領、そしてトークン交換のプロセスを行う。`get_dropbox_link.py`は、このリフレッシュトークンでDropbox認証を維持し、指定されたローカルファイルパスからDropbox API経由で共有リンク(raw=1形式)を取得する。最後に、`send_request.py`は、取得したDropboxリンクとファイル名などの情報をGASエンドポイントへPOST送信し、MacとGoogleスプレッドシート間のデータ連携を自動化する。これらのスクリプトはM1/M2 Macの両方に対応している。 M2Mac ▫️DropboxのAPIからトークン取得 Python <任意のフォルダパス>/dropbox_auth_setup.py dropbox_auth_setup.py ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ dropbox_auth_setup.py (完全env適合化・M1/M2両対応決定版) - 金庫(.env)から APP_KEY / APP_SECRET を安全にロードして認証を自動開始します。 - 物理パスのハードコードを完全排除した、最高水準のポータブル設計です。 """ import os import sys import webbrowser import http.server import socketserver import urllib.parse import requests from pathlib import Path from dotenv import load_dotenv # ===== 1. 環境設定(金庫 .env からの動的ロード & 環境差の自動解決) ===== dotenv_loaded = False for dotenv_path in [ Path(__file__).resolve().parent / ".env", Path(__file__).resolve().parent.parent / ".env", Path.home() / "python_scripts" / ".env" ]: if dotenv_path.exists(): load_dotenv(dotenv_path) dotenv_loaded = True break # 環境変数から機密情報を取得 APP_KEY = os.getenv("APP_KEY") APP_SECRET = os.getenv("APP_SECRET") REDIRECT_URI = "http://localhost:8080" AUTH_URL = "https://www.dropbox.com/oauth2/authorize" TOKEN_URL = "https://api.dropboxapi.com/oauth2/token" # 【自律停止ガード】必要な環境変数が不足している場合は即座に安全に処理を停止 if not APP_KEY or not APP_SECRET: print("❌ [dropbox_auth_setup.py] 金庫(.env)に必要な APP_KEY / APP_SECRET が設定されていません。安全に自律停止します。", file=sys.stderr) sys.exit(1) # 📡 Step 1: 認証コードを取得する class AuthorizationHandler(http.server.SimpleHTTPRequestHandler): def do_GET(self): self.send_response(200) self.send_header("Content-type", "text/html; charset=utf-8") self.end_headers() query = urllib.parse.urlparse(self.path).query params = urllib.parse.parse_qs(query) if "code" in params: code = params["code"][0] self.wfile.write("

✅ 認証に成功しました!ターミナルに戻ってトークンを確認してください。

".encode("utf-8")) print(f"\n📡 認証コードを受信しました: {code}") # 🚀 Step 2: 認証コードをトークンと交換する print("🔄 トークンへの交換をリクエスト中...") res = requests.post(TOKEN_URL, data={ "code": code, "grant_type": "authorization_code", "client_id": APP_KEY, "client_secret": APP_SECRET, "redirect_uri": REDIRECT_URI }) if res.status_code != 200: print("❌ 認証に失敗しました:", res.text) sys.exit(1) tokens = res.json() print("\n================================================================================") print("🎉 認証情報を取得しました!以下の3行を .env へ登録・上書きしてください。") print("================================================================================") print(f"APP_KEY = ""{APP_KEY}\"") print(f"APP_SECRET = ""{APP_SECRET}\"") print(f"REFRESH_TOKEN = ""{tokens.get('refresh_token', '')}\"") print("================================================================================\n") # サーバーを安全に自律停止 sys.exit(0) def main(): auth_link = f"{AUTH_URL}?client_id={APP_KEY}&redirect_uri={urllib.parse.quote(REDIRECT_URI)}&response_type=code&token_access_type=offline" print("🌐 ブラウザを開いて認証を実行します。自動で開かない場合は、以下のURLを直接開いてください。") print(f"\n👉 {auth_link}\n") webbrowser.open(auth_link) # ローカルサーバーを起動して認証コードのコールバック(戻り)を待つ socketserver.TCPServer.allow_reuse_address = True with socketserver.TCPServer(("", 8080), AuthorizationHandler) as httpd: print("⏳ 待機中... (ブラウザでの認証完了をお待ちしております)") try: httpd.handle_request() except KeyboardInterrupt: print("\n🛑 キャンセルされました。") if __name__ == "__main__": main() ``` get_dropbox_link.py <任意のフォルダパス>/get_dropbox_link.py <任意のフォルダパス>/get_dropbox_link.py ◆M2 Mac用 ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ get_dropbox_link.py (完全env適合化・M1/M2両対応決定版) - .envから認証キー・リフレッシュトークンを動的にロード。 - 旧パスから新パス「mybox/mybox_1」へ自動追従し、Dropbox API経由で安全に共有リンク(raw=1)を取得。 - 万が一のDropbox通信エラー(アプリ停止やAPI遮断時)は、空文字を出力して「空欄フォールバック」を起動。 """ import sys import json import re import os import dropbox from pathlib import Path from dotenv import load_dotenv # ===== 1. 環境設定(金庫 .env からの動的ロード & 環境差の自動解決) ===== dotenv_loaded = False for dotenv_path in [ Path(__file__).resolve().parent / ".env", Path(__file__).resolve().parent.parent / ".env", Path.home() / "python_scripts" / ".env" ]: if dotenv_path.exists(): load_dotenv(dotenv_path) dotenv_loaded = True break # 環境変数から機密情報を取得 APP_KEY = os.getenv("APP_KEY") APP_SECRET = os.getenv("APP_SECRET") REFRESH_TOKEN = os.getenv("REFRESH_TOKEN") # 【自律停止ガード】必要な環境変数が不足している場合は即座に安全に処理を停止 if not APP_KEY or not APP_SECRET or not REFRESH_TOKEN: print("❌ [get_dropbox_link.py] 金庫(.env)に必要なDropbox APIキーが設定されていません。安全に自律停止します。", file=sys.stderr) sys.exit(1) # 🔁 Dropboxオブジェクトを取得(リフレッシュ対応) def get_dbx(): return dropbox.Dropbox( oauth2_refresh_token=<個別のトークン文字列>, app_key=APP_KEY, app_secret=APP_SECRET ) # 🔧 Dropbox内の相対パスを取得(M1・M2・外付けSSDの全パスへ自動追従) def get_dropbox_path(local_path): possible_roots = [ "<任意のフォルダパス>", "<任意のフォルダパス>", "/Volumes/NO3_SSD/mybox/mybox_1" ] for root in possible_roots: if local_path.startswith(root): relative_path = local_path[len(root):] return "/dropbox_1" + relative_path # 💡 修正箇所:Dropboxフォルダ外でも強制終了せず、ログを残して空文字を返す print(f"⚠️ 通知: ファイルがDropboxフォルダ内にありません\nlocal_path: {local_path}", file=sys.stderr) return "" # 🔗 共有リンクを取得または作成 def get_shared_link(dbx, dbx_path): if not dbx_path: return "" 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: pass # なければ新規作成 try: link = dbx.sharing_create_shared_link_with_settings(dbx_path) return convert_to_raw_url(link.url) except Exception as e: print("❌ Dropbox APIで共有リンクを作成できませんでした:", file=sys.stderr) print(e, file=sys.stderr) return "" # 🔄 dl=0 → raw=1 変換 def convert_to_raw_url(url): return re.sub(r'dl=0', 'raw=1', url) # 🏁 実行エントリポイント if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: get_dropbox_link.py ", file=sys.stderr) sys.exit(1) local_path = sys.argv[1] try: dbx_path = get_dropbox_path(local_path) if dbx_path == "": # パスが空(Dropbox外)の場合はAPI通信をスキップして空行出力(フォールバック) print("") else: print(f"📂 Dropbox API 送信パス = {dbx_path}", file=sys.stderr) dbx = get_dbx() link = get_shared_link(dbx, dbx_path) print(link) except Exception as e: print(f"⚠️ Dropbox通信エラー(フォールバック実行): {e}", file=sys.stderr) print("") # 空行を出力してAppleScript側のクラッシュを防止 ``` ◆M1 Mac用 ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ get_dropbox_link.py (完全env適合化・M1/M2両対応決定版) - .envから認証キー・リフレッシュトークンを動的にロード。 - 旧パスから新パス「mybox/mybox_1」へ自動追従し、Dropbox API経由で安全に共有リンク(raw=1)を取得。 - 万が一のDropbox通信エラー(アプリ停止やAPI遮断時)は、空文字を出力して「空欄フォールバック」を起動。 """ import sys import json import re import os import dropbox from pathlib import Path from dotenv import load_dotenv # ===== 1. 環境設定(金庫 .env からの動的ロード & 環境差の自動解決) ===== dotenv_loaded = False for dotenv_path in [ Path(__file__).resolve().parent / ".env", Path(__file__).resolve().parent.parent / ".env", Path.home() / "python_scripts" / ".env" ]: if dotenv_path.exists(): load_dotenv(dotenv_path) dotenv_loaded = True break # 環境変数から機密情報を取得 APP_KEY = os.getenv("APP_KEY") APP_SECRET = os.getenv("APP_SECRET") REFRESH_TOKEN = os.getenv("REFRESH_TOKEN") # 【自律停止ガード】必要な環境変数が不足している場合は即座に安全に処理を停止 if not APP_KEY or not APP_SECRET or not REFRESH_TOKEN: print("❌ [get_dropbox_link.py] 金庫(.env)に必要なDropbox APIキーが設定されていません。安全に自律停止します。", file=sys.stderr) sys.exit(1) # 🔁 Dropboxオブジェクトを取得(リフレッシュ対応) def get_dbx(): return dropbox.Dropbox( oauth2_refresh_token=<個別のトークン文字列>, app_key=APP_KEY, app_secret=APP_SECRET ) # 🔧 Dropbox内の相対パスを取得(M1・M2・外付けSSDの全パスへ自動追従) def get_dropbox_path(local_path): possible_roots = [ "<任意のフォルダパス>", "<任意のフォルダパス>", "/Volumes/NO3_SSD/mybox/mybox_1" ] for root in possible_roots: if local_path.startswith(root): relative_path = local_path[len(root):] return "/dropbox_1" + relative_path # 💡 修正箇所:Dropboxフォルダ外でも強制終了せず、ログを残して空文字を返す print(f"⚠️ 通知: ファイルがDropboxフォルダ内にありません\nlocal_path: {local_path}", file=sys.stderr) return "" # 🔗 共有リンクを取得または作成 def get_shared_link(dbx, dbx_path): if not dbx_path: return "" 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: pass # なければ新規作成 try: link = dbx.sharing_create_shared_link_with_settings(dbx_path) return convert_to_raw_url(link.url) except Exception as e: print("❌ Dropbox APIで共有リンクを作成できませんでした:", file=sys.stderr) print(e, file=sys.stderr) return "" # 🔄 dl=0 → raw=1 変換 def convert_to_raw_url(url): return re.sub(r'dl=0', 'raw=1', url) # 🏁 実行エントリポイント if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: get_dropbox_link.py ", file=sys.stderr) sys.exit(1) local_path = sys.argv[1] try: dbx_path = get_dropbox_path(local_path) if dbx_path == "": # パスが空(Dropbox外)の場合はAPI通信をスキップして空行出力(フォールバック) print("") else: print(f"📂 Dropbox API 送信パス = {dbx_path}", file=sys.stderr) dbx = get_dbx() link = get_shared_link(dbx, dbx_path) print(link) except Exception as e: print(f"⚠️ Dropbox通信エラー(フォールバック実行): {e}", file=sys.stderr) print("") # 空行を出力してAppleScript側のクラッシュを防止 ``` ▫️macとspreadsheetのデータ交換 Python send_request.py <任意のフォルダパス>/send_request.py <任意のフォルダパス>/send_request.py ◆M1 Mac ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ send_request.py (完全env適合化・M1/M2両対応決定版) - .envから GAS_URL を動的にロード。 - 機密ドメインパスを1文字も残さない完璧なクレンジング設計です。 """ import sys import os import json import requests from pathlib import Path from dotenv import load_dotenv # ===== 1. 環境設定(金庫 .env からの動的ロード & 環境差の自動解決) ===== dotenv_loaded = False for dotenv_path in [ Path(__file__).resolve().parent / ".env", Path(__file__).resolve().parent.parent / ".env", Path.home() / "python_scripts" / ".env" ]: if dotenv_path.exists(): load_dotenv(dotenv_path) dotenv_loaded = True break # 環境変数からGAS_URLをロード GAS_URL = os.getenv("GAS_URL") # 【自律停止ガード】必要な環境変数が不足している場合は即座に安全に処理を停止 if not GAS_URL: print("❌ [send_request.py] 金庫(.env)に GAS_URL が設定されていません。安全に自律停止します。", file=sys.stderr) sys.exit(1) # 🔍 受け取った引数を確認 print("📥 受信引数:", sys.argv) # コマンドライン引数を取得 if len(sys.argv) < 5: print("❌ 引数が不足しています") sys.exit(1) dropbox_link, file_path, file_name, extn = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] # 🔍 受け取ったデータをログに記録 print("📥 受信データ from AppleScript:") print(f" dropboxLink: {dropbox_link}") print(f" filePath: {file_path}") print(f" fileName: {file_name}") print(f" extn: {extn}") # 送信データ data = { "dropboxLink": dropbox_link, "filePath": file_path, "fileName": file_name, "updateFlag": "ADD" } # 🔍 送信データを確認 print("📤 送信データ:", json.dumps(data, indent=2, ensure_ascii=False)) # GAS にデータを送信 try: response = requests.post(GAS_URL, json=data) response.raise_for_status() gas_response = response.json() print("📥 GAS レスポンス:", json.dumps(gas_response, indent=2, ensure_ascii=False)) if gas_response.get("status") == "success": print("✅ GAS にデータが正常に送信されました!") print("success") else: print(f"❌ GAS でエラー発生: {gas_response.get('message')}") print("error") except requests.exceptions.RequestException as e: print(f"❌ GAS 送信エラー: {e}") print("error") ``` ◆M2 Mac ``` #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ send_request.py (完全env適合化・M1/M2両対応決定版) - .envから GAS_URL を動的にロード。 - 機密ドメインパスを1文字も残さない完璧なクレンジング設計です。 """ import sys import os import json import requests from pathlib import Path from dotenv import load_dotenv # ===== 1. 環境設定(金庫 .env からの動的ロード & 環境差の自動解決) ===== dotenv_loaded = False for dotenv_path in [ Path(__file__).resolve().parent / ".env", Path(__file__).resolve().parent.parent / ".env", Path.home() / "python_scripts" / ".env" ]: if dotenv_path.exists(): load_dotenv(dotenv_path) dotenv_loaded = True break # 環境変数からGAS_URLをロード GAS_URL = os.getenv("GAS_URL") # 【自律停止ガード】必要な環境変数が不足している場合は即座に安全に処理を停止 if not GAS_URL: print("❌ [send_request.py] 金庫(.env)に GAS_URL が設定されていません。安全に自律停止します。", file=sys.stderr) sys.exit(1) # 🔍 受け取った引数を確認 print("📥 受信引数:", sys.argv) # コマンドライン引数を取得 if len(sys.argv) < 5: print("❌ 引数が不足しています") sys.exit(1) dropbox_link, file_path, file_name, extn = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] # 🔍 受け取ったデータをログに記録 print("📥 受信データ from AppleScript:") print(f" dropboxLink: {dropbox_link}") print(f" filePath: {file_path}") print(f" fileName: {file_name}") print(f" extn: {extn}") # 送信データ data = { "dropboxLink": dropbox_link, "filePath": file_path, "fileName": file_name, "updateFlag": "ADD" } # 🔍 送信データを確認 print("📤 送信データ:", json.dumps(data, indent=2, ensure_ascii=False)) # GAS にデータを送信 try: response = requests.post(GAS_URL, json=data) response.raise_for_status() gas_response = response.json() print("📥 GAS レスポンス:", json.dumps(gas_response, indent=2, ensure_ascii=False)) if gas_response.get("status") == "success": print("✅ GAS にデータが正常に送信されました!") print("success") else: print(f"❌ GAS でエラー発生: {gas_response.get('message')}") print("error") except requests.exceptions.RequestException as e: print(f"❌ GAS 送信エラー: {e}") print("error") ```