👔

Docker版RcloneでGoogleDriveの認証情報を取得・rcloneコマンド実行 (headless版)

2023/12/29に公開

Docker版RcloneでGoogleDriveの認証情報を取得・rcloneコマンド実行のheadless版

TL;DR

  • Seleniumをheadless起動で認証操作

背景

  • 何が何でもGUIを使いたくない
  • 自動認証したい

※圧倒的にGUI手動認証のほうが楽なのでお勧めできない。

手順

  • 認証自動化Pythonスクリプトの準備

詳細後述

auto_rclone.py
import os
import shutil
from argparse import ArgumentParser, Namespace
from signal import SIGTERM
from tempfile import TemporaryDirectory

from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from undetected_chromedriver import Chrome


class AutoRclone:
    def __init__(
        self,
        user_data_dir: str,
        driver_executable_path: str,
        browser_executable_path: str,
    ) -> None:
        options: Options = Options()
        options.add_argument("--headless")
        options.add_argument("--no-sandbox")
        self.chrome: Chrome = Chrome(
            options=options,
            user_data_dir=user_data_dir,
            driver_executable_path=driver_executable_path,
            browser_executable_path=browser_executable_path,
        )

    def google_drive(self, url: str, email: str, password: str) -> bool:
        print("get", url)
        self.chrome.get(url)

        print("email")
        email_element: WebElement = WebDriverWait(self.chrome, 15).until(
            EC.element_to_be_clickable((By.NAME, "identifier"))
        )
        email_element.send_keys(email)
        self.chrome.find_element(By.ID, "identifierNext").click()

        print("password")
        password_element: WebElement = WebDriverWait(self.chrome, 15).until(
            EC.element_to_be_clickable((By.NAME, "Passwd"))
        )
        password_element.send_keys(password)
        self.chrome.find_element(By.ID, "passwordNext").click()

        print("approve")
        approve_element: WebElement = WebDriverWait(self.chrome, 15).until(
            EC.element_to_be_clickable((By.ID, "submit_approve_access"))
        )
        approve_element.click()

        print("result")
        result_element: WebElement = WebDriverWait(self.chrome, 15).until(
            EC.presence_of_element_located((By.TAG_NAME, "pre"))
        )
        return "All done. Please go back to rclone." in result_element.text

    def quit(self) -> None:
        print("quit")
        os.kill(self.chrome.browser_pid, SIGTERM)


def auto_rclone(args: Namespace) -> None:
    with TemporaryDirectory(ignore_cleanup_errors=True) as dname:
        rclone: AutoRclone = AutoRclone(
            dname,
            args.driver,
            args.chrome,
        )
        try:
            if args.google_drive:
                print(
                    "auth google drive:",
                    rclone.google_drive(
                        args.url,
                        os.environ["GOOGLE_EMAIL"],
                        os.environ["GOOGLE_PASSWORD"],
                    ),
                )
            else:
                print("nothing to do")
        finally:
            rclone.quit()
    shutil.rmtree(dname) if os.path.exists(dname) else None


if __name__ == "__main__":
    parser: ArgumentParser = ArgumentParser()
    parser.add_argument("url", help="target url")
    parser.add_argument("driver", help="driver path")
    parser.add_argument("chrome", help="chrome path")
    parser.add_argument(
        "-gd", "--google_drive", action="store_true", help="auth google drive"
    )
    auto_rclone(parser.parse_args())
  • 環境変数ファイルの準備

.env
GOOGLE_EMAIL=アカウント@gmail.com
GOOGLE_PASSWORD=パスワード
  • Pythonコンテナ起動

command
docker run -itd --name rclonepython -v ./auto_rclone.py:/root/auto_rclone.py --env-file=./.env python:3.12.1-bookworm
  • 必要機能インストール&rclone実行

command
docker exec -it --workdir=/root/ rclonepython bash -c "wget https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip && \
  unzip chromedriver_linux64.zip && \
  wget https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/114.0.5735.90/linux64/chrome-linux64.zip && \
  unzip chrome-linux64.zip && \
  apt update && \
  apt install -y \
    rclone \
    libnss3 \
    libdbus-1-3 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libxkbcommon0 \
    libxcomposite1 \
    libxdamage1 \
    libxfixes3 \
    libxrandr2 \
    libgbm1 \
    libasound2 && \
  pip install undetected-chromedriver==3.5.4 && \
  rclone config"

以下ログが出るまで進める。

command
****/**/** **:**:** NOTICE: If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth?state=***
****/**/** **:**:** NOTICE: Log in and authorize rclone for access
****/**/** **:**:** NOTICE: Waiting for code...
  • 認証自動化スクリプト実行

別ターミナルを開き、上記URLを用いて以下実行

command
docker exec -it rclonepython python /root/auto_rclone.py http://127.0.0.1:53682/auth?state=*** /root/chromedriver /root/chrome-linux64/chrome -gd

以下のようなログが出れば認証成功

could not detect version_main.therefore, we are assuming it is chrome 108 or higher
get http://127.0.0.1:53682/auth?state=***
email
password
approve
result
auth google drive: True
quit

認証待ち側のターミナルが進められるようになる。

****/**/** **:**:** NOTICE: Got code
Configure this as a Shared Drive (Team Drive)?

y) Yes
n) No (default)
y/n>

Configuration complete.
Options:
- type: drive
- scope: drive
- token: {"access_token":"アクセストークン","token_type":"Bearer","refresh_token":"リフレッシュトークン","expiry":"有効期限"}
- team_drive:
Keep this "gd" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d>

Current remotes:

Name                 Type
====                 ====
gdrive               drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q>

スクリプト説明

  • ブラウザ自動化を検知されるとパスワード入力ができなくなるので、undetected_chromedriverを使用
from undetected_chromedriver import Chrome
  • Google Drive以外でも使うかもしれないことを想定して、引数-gdが与えられた時のみ実行
    parser.add_argument(
        "-gd", "--google_drive", action="store_true", help="auth google drive"
    )
  • ユーザーデータは毎回初期化するために一時ディレクトリを使用
    with TemporaryDirectory(ignore_cleanup_errors=True) as dname:
        rclone: AutoRclone = AutoRclone(
            dname,
  • メール・パスワードはコマンドに残したくないので環境変数から読み込む
                    rclone.google_drive(
                        args.url,
                        os.environ["GOOGLE_EMAIL"],
                        os.environ["GOOGLE_PASSWORD"],
                    ),
  • ログイン・許可の実際の画面要素を取得しながら操作
    ※仕様変更されたり本人確認挟まれたりしたら動作しない。
def google_drive(self, url: str, email: str, password: str) -> bool:
    ・・・
  • Chrome.quit()だとプロセスが残ったりするのでkill(プロファイルも削除するため)
os.kill(self.chrome.browser_pid, SIGTERM)
  • TemporaryDirectoryのブロックを抜ける時にディレクトリが削除できない場合があるので、抜けた後に削除
shutil.rmtree(dname) if os.path.exists(dname) else None

Discussion