目次 このページのソースコードを表示

rcloneを用いてFTPによるファイルのアップロードとダウンロードをGitHub Actions上で行う

公開日:
更新日:

本稿では、外部アクションに頼らずrcloneをもちいた外部サーバとのファイル転送方法を説明します。rcloneとは、FTPだけに限らず外部サーバに対してファイルのアップロードやダウンロード、削除や移動といったファイル操作を行えるCLIツールです。rcloneをGitHub Actionsで使うことにより、特殊な要求にも柔軟に対応できるようになります。

はじめに

GitHubには、リポジトリへの変更をトリガーとしてさまざまな処理を自動で行うGitHub Actionsという機能があります[1]。これまで、GitHub Actionsを使って、静的サイトなどの外部ホスティングサーバーに記事をアップロードする方法が紹介されてきました[2][3]。紹介の中で、FTPサーバへのアップロードを行う専用のアクションを用いた方法がよく使われています。

専用アクションを用いた方法だと、複雑なことをするときに実現が困難になる場合があります。たとえば、先ほど紹介したアクションでは、FTPへのアップロードができても、ダウンロードはできません。特定のファイルのみアップロードすることも難しいです。

本稿では、外部アクションに頼らずrcloneをもちいた外部サーバとのファイル転送方法を説明します。rcloneとは、FTPだけに限らず外部サーバに対してファイルのアップロードやダウンロード、削除や移動といったファイル操作を行えるCLIツールです。rcloneをGitHub Actionsで使うことにより、特殊な要求にも柔軟に対応できるようになります。

rcloneとは

rcloneは、クラウドストレージのファイルを管理するオープンソースなコマンドラインプログラムです[4]。rcloneは、S3オブジェクトストレージやビジネスおよび個人向けのファイルストレージサービスなど、70種類以上のクラウドストレージに対応しています[4]。また、FTPやSFTPなどの標準的な転送プロトコルもサポートしています[4]

GitHub Actionsでrcloneを使用する

GitHub Actionsで、rcloneを使用できるために、まずrcloneバイナリをジョブマシンにインストールします。

ジョブマシンがLinux/macOS/BSDシステムの場合、以下のコマンドをステップ内で実行します。

        sudo -v ; curl https://rclone.org/install.sh | sudo bash
そのほかのインストール方法

rcloneはこれ以外のインストール方法も提供されています。WindowsやDockerにインストールしたい場合は、以下の公式ドキュメントを参考にしてください。

実際にジョブファイルの内容は以下のようになります。

        jobs:
          your-job:
            runs-on: ubuntu-latest
              steps:
              - run: |
                  # install rclone
                  sudo -v ; curl https://rclone.org/install.sh | sudo bash
                  # ...

これだけです。このステップ以降で、rcloneコマンドを使えます。

rcloneがFTPサーバと接続するための設定を行う

rcloneがFTPサーバと接続するための設定を行います。

FTPサーバ情報を確認する

FTPサーバに接続するためには、以下のサーバ情報が必要です。お使いのFTPサーバ管理画面から情報を確認してください。

  • FTPサーバ名

    例: example.com

  • アカウント名

    FTPサーバへログインするためのアカウント名。

  • パスワード

    FTPサーバへログインするためのパスワード。

rcloneの設定

rcloneに接続先のFTPサーバを設定しなければなりません。よくされるrcloneの設定方法に、rclone configコマンドを使う方法があります[5]。ですが、configコマンドは対話ベースで設定していくため、スクリプトで設定するには相性が悪いです。

rcloneでは、configコマンドを用いずに、環境変数で設定する方法が提供されています[6]。環境変数の仕様は、RCLONE_{リモート名}_{設定名}です。すべての文字は大文字でなければなりません。リモート名は、設定したオンラインストレージを識別するためのもので、自由に決められます。

設定例は以下の通りです。

            RCLONE_CONFIG_FTP_TYPE="ftp"
            RCLONE_CONFIG_FTP_HOST="example.com"
            RCLONE_CONFIG_FTP_USER="your-name"
            RCLONE_CONFIG_FTP_PASS="x0eOjb0dEzC0XhYHawfy6pjC9oA"
            RCLONE_CONFIG_FTP_TLS="false"
            RCLONE_CONFIG_FTP_EXPLICIT_TLS="true"

各設定項目の説明は以下の通りです。

TYPE

今回ファイルサーバはFTPサーバを想定しているので、ftpとします。

HOST

FTPサーバ名。

USER

FTPサーバへログインするためのアカウント名。

PASS

FTPサーバへログインするためのハッシュ化されたパスワード。

rcloneでは、パスワードを平文で扱いません。パスワードをハッシュ化する必要があります。ジョブランナー上でなく、今自身が操作しているローカル環境にrcloneをインストールし、rclone obscureコマンドで平文パスワードをハッシュ化してください。

TLS

FTPサーバと暗黙的にTLS通信する(Implicit FTPS)かどうか。

クライアントは、接続先のサーバがFTPSか確認せずに、はじめからTLSで通信を行います(暗黙的)[7]。Implicit FTPSは、非推奨といわれている[7]ので、後述するExplicit FTPSを使用することをお勧めします。

EXPLICIT_TLS

FTPサーバと明示的にTLS通信する(Explicit FTPS)かどうか。

クライアントはまず、接続先サーバにTLS通信を行うことを要求して、そのあとに暗号化のもとでFTP通信を行います(明示的)[7]

GitHub SecretsにFTPサーバ情報を追加する

サーバ情報をGitHub Actionsスクリプト上でrcloneに指定したいわけですが、セキュリティの面でそのままスクリプトに書いてはいけません。

GitHub Actionsには、アクション内でセンシティブな情報を扱うためのSecretsという機能があります。Secretsは、組織やリポジトリ、リポジトリ環境で作成する変数のことです[8]。作成したSecretsは GitHub Actionsのワークフローで使用できます[8]

GitHub Secretsに、FTPサーバ情報を追加しましょう。"GitHubリポジトリ設定"→"Secrets and variables"[注 1]から以下の変数を追加します。

変数名 内容
FTP_SERVER FTPサーバ名
FTP_USERNAME アカウント名
FTP_PASSWORD パスワード

設定した値は、ワークフローのスクリプト内で${{ secrets.<変数名> }}とするとアクセスできます。

GitHub ActionsでrcloneのFTPサーバ設定を行う

以上を踏まえて、rcloneがFTPサーバと接続できるようにするワークフロースクリプトは以下のようになります。リモート名はftpで、Explicit FTPSで接続しています。

            jobs:
              your-job:
                runs-on: ubuntu-latest
                env: # (1)
                  RCLONE_CONFIG_FTP_TYPE: ftp
                  RCLONE_CONFIG_FTP_HOST: ${{ secrets.FTP_SERVER }}
                  RCLONE_CONFIG_FTP_USER: ${{ secrets.FTP_USERNAME }}
                  RCLONE_CONFIG_FTP_PASS: ${{ secrets.FTP_PASSWORD }}
                  RCLONE_CONFIG_FTP_TLS: false
                  RCLONE_CONFIG_FTP_EXPLICIT_TLS: true
                steps:
                - run: |
                    # install rclone
                    sudo -v ; curl https://rclone.org/install.sh | sudo bash
                    # ...
(1)

envは、ジョブのステップ内で環境変数として使うことができる変数のマップです。

FTPサーバにファイルをアップロードする

rcloneを用いてftpサーバにアップロードしてみましょう。

rclone syncコマンドは、転送元のファイルを転送先へ同期します[9]。クライアントのカレントディレクトリの内容をリモート名ftp上のパス/home/username/www/下に配置するには、以下のようにします。

        rclone sync ./ ftp:/home/username/www/

このコマンドは、たとえば以下のようにローカル環境のカレントディレクトリが構成されていた時、

  • ./
    • file-a
    • directory-a
    • ...

リモート側は次のようにコピーされます。

  • /home/username/www/
    • file-a
    • directory-a
    • ...

以上を踏まえて、リポジトリのファイルをリモートのFTPサーバにアップロードするワークフロースクリプトは以下のようになります。

        jobs:
          your-job:
            runs-on: ubuntu-latest
            env:
                RCLONE_CONFIG_FTP_TYPE: ftp
                RCLONE_CONFIG_FTP_HOST: ${{ secrets.FTP_SERVER }}
                RCLONE_CONFIG_FTP_USER: ${{ secrets.FTP_USERNAME }}
                RCLONE_CONFIG_FTP_PASS: ${{ secrets.FTP_PASSWORD }}
                RCLONE_CONFIG_FTP_TLS: false
                RCLONE_CONFIG_FTP_EXPLICIT_TLS: true
              steps:
              # 最新の内容をチェックアウト
              - uses: actions/checkout@v3
              # rcloneによるファイルアップロード
              - run: |
                  # install rclone
                  sudo -v ; curl https://rclone.org/install.sh | sudo bash
                  # upload files
                  rclone sync ./ ftp:/home/username/www/
リモート側のファイルがローカル側のファイルで上書きされてしまう

rclone syncコマンドは、基本的に、転送元と転送先のファイルをファイルサイズと更新時間の両方またはMD5SUMのいずれかで検証し、内容が同じであれば、ファイルを転送しません[9]。しかし、MD5SUMに対応していないFTPサーバもあります。その場合、rcloneはファイルの更新日時とサイズで検証しますが、ジョブごとにリポジトリからファイルをクローンしてくるため、ファイルの更新日時はFTPサーバのよりも新しいタイムスタンプになります。FTPサーバのよりも常に新しいタイムスタンプで更新日時がされてしまいます。つまり、ジョブ側のファイルが常に最新と誤認し、すべてのファイルが転送対象となり、リモート側のファイルはローカル側のファイルで上書きされてしまいます。

リモートのファイルがローカルのファイルで上書きされるのは、同期したといえるので、ほとんどの場合で問題になりません。ですが、内容が変わっていないのに、ファイルの更新日時だけが変わってしまうと、困ることがあります。たとえば、ファイルの更新日時で新規コンテンツのリストを作成しているとします。このとき、ジョブからアップロードされるたびにリストが初期化されてしまいます。

のちの応用例で、ファイルをアップロードするたびに、更新日時が書き換えられてしまう問題を解決する方法を説明します。

FTPサーバからファイルをダウンロードする

rcloneを用いてFTPサーバからファイルをダウンロードしてみましょう。アップロードの時と同じように、rclone syncコマンドを使いますが、転送先と転送元を入れ替えます。

FTPサーバからファイルをダウンロードするワークフロースクリプトは以下のようになります。

        jobs:
          your-job:
            runs-on: ubuntu-latest
            env:
                RCLONE_CONFIG_FTP_TYPE: ftp
                RCLONE_CONFIG_FTP_HOST: ${{ secrets.FTP_SERVER }}
                RCLONE_CONFIG_FTP_USER: ${{ secrets.FTP_USERNAME }}
                RCLONE_CONFIG_FTP_PASS: ${{ secrets.FTP_PASSWORD }}
                RCLONE_CONFIG_FTP_TLS: false
                RCLONE_CONFIG_FTP_EXPLICIT_TLS: true
              steps:
              # 最新の内容をチェックアウト
              - uses: actions/checkout@v3
              # rcloneによるファイルアップロード
              - run: |
                  # install rclone
                  sudo -v ; curl https://rclone.org/install.sh | sudo bash
                  # download files
                  rclone sync ftp:/home/username/www/ ./

応用例

FTPサーバから最新の内容を取得して、Gitリポジトリに差分をプッシュする

機能

  • FTPサーバから最新の内容を取得し、mainブランチにコミット
  • mainブランチへのプルリクエストが作成されたときに開始
  • 手動で実行が可能

この機能が必要となった経緯

  • あるWebサイトがあり、そのサイトはサイト内のコンテンツをWebページにする
  • コンテンツは、サイト上で編集できる
  • サイト外でも編集できるように、複製したサイト内コンテンツをGitHubリポジトリで管理した
  • Webサイト上で行った変更をGitリポジトリに反映する必要があった

スクリプト

                name: sync

                on:
                  # (1)
                  pull_request:
                    branches: ["main"]
                    types: [opened, reopened]
                  # (2)
                  workflow_dispatch:

                # (3)
                concurrency:
                  ${{ github.workflow }}

                jobs:
                  sync-from-site:
                    name: Fetch the latest document from the site
                    runs-on: ubuntu-latest
                    env:
                      RCLONE_CONFIG_FTP_TYPE: ftp
                      RCLONE_CONFIG_FTP_HOST: ${{ secrets.FTP_SERVER }}
                      RCLONE_CONFIG_FTP_USER: ${{ secrets.FTP_USERNAME }}
                      RCLONE_CONFIG_FTP_PASS: ${{ secrets.FTP_PASSWORD }}
                      RCLONE_CONFIG_FTP_TLS: false
                      RCLONE_CONFIG_FTP_EXPLICIT_TLS: true
                      FTP_SERVER_DIR: ${{ secrets.FTP_SERVER_DIR }}
                    steps:
                    - name: Fetch main branch
                      uses: actions/checkout@v3
                      with:
                        ref: 'main'
                    - name: Fetch the latest document
                      run: |
                        # rcloneのインストール
                        sudo -v ; curl https://rclone.org/install.sh | sudo bash

                        # (4)
                        rclone sync ftp:${FTP_SERVER_DIR} ./ --exclude="/.git**" -v
                    - name: Push the changes to the main branch
                      run: |
                        # (5)
                        git remote set-url origin https://github-actions:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}
                        git config --global user.name "${GITHUB_ACTOR}"
                        git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
                        git add . # (6)
                        git diff --cached --exit-code || { # (7)
                          # (8)
                          git commit -m "Sync from site"
                          git push origin main
                        }

解説

(1)

mainブランチへのプルリクエストが新規または再オープンされたときに開始。

(2)

手動でワークフローを起動できるように設定。

GitHub Actions/ワークフローをトリガーするイベント/workflow_dispatch

(3)

このワークフローが重複で起動しないようにします。複数のジョブが一つのFTPサーバに接続して、サーバに負荷がかかることを防ぎます。

(4)

FTPサーバからファイルを同期します。

--exclude="/.git**"オプションで、ルート直下の.gitから始まるファイルまたはフォルダは同期の対象外にします。git関連のファイルは、Webサイト側では関係がないので、不要なファイルを意味もなく同期して、トラブルになるのを防ぎます。

-vオプションで、同期中の進捗状況を標準出力に表示します。

(5)

GitHub Actionsから、リモートリポジトリにプッシュするための設定です。

(6)

変更をコミットの前段階であるステージ状態にします。仮にコミットする変更がない場合でも、コマンド自体は失敗しません。

(7)

コミットできる変更があるかどうかを確認します。

--cachedオプションは、ステージされている変更を見るときに使います。

--exit-codeオプションを指定すると、変更があるときコマンドはコード1(異常)で終了し、変更がないときコード0(正常)で終了します。

(8)

変更があった場合、mainブランチにコミットし、リモートにプッシュします。

プルリクエストでmainブランチにマージされたときに、差分をFTPサーバに反映する

機能

  • プルリクエストでmainブランチにマージされたときに開始する
  • 差分のみFTPサーバへ転送

この機能が必要となった経緯

  • FTPサーバにアップロードするときに、サーバ上のすべてのファイルが上書きされてしまう
  • ファイルの内容は変わっていないのに、更新日時がアップロード日時に書き変わってしまう
  • Webサイトでは、ファイルの更新日時を見て変更があったファイルを一覧で表示している
  • ファイルの内容が変わっていないのに、更新一覧に表示されてしまう

スクリプト

                name: publish

                on:
                  # (1)
                  pull_request:
                    branches: ["main"]
                    types: [closed]

                jobs:
                  publish-to-site:
                    name: Publish the document to the site
                    runs-on: ubuntu-latest
                    # (2)
                    if: github.event.pull_request.merged == true
                    env:
                      RCLONE_CONFIG_FTP_TYPE: ftp
                      RCLONE_CONFIG_FTP_HOST: ${{ secrets.FTP_SERVER }}
                      RCLONE_CONFIG_FTP_USER: ${{ secrets.FTP_USERNAME }}
                      RCLONE_CONFIG_FTP_PASS: ${{ secrets.FTP_PASSWORD }}
                      RCLONE_CONFIG_FTP_TLS: false
                      RCLONE_CONFIG_FTP_EXPLICIT_TLS: true
                      FTP_SERVER_DIR: ${{ secrets.FTP_SERVER_DIR }}
                    steps:
                      # (3)
                      - name: Checkout merge-commit
                        uses: actions/checkout@v3
                        with:
                          ref: ${{ github.event.pull_request.merge_commit_sha }}
                          fetch-depth: 2
                      - name: Upload files to the site
                        run: |
                          # rcloneのインストール
                          sudo -v ; curl https://rclone.org/install.sh | sudo bash

                          options='-v'

                          # (4)
                          git diff ${{ github.event.pull_request.merge_commit_sha }}^ --name-status | {
                            while read line; do # (5)
                              set $line # (6)
                              case $1 in
                                R*)
                                  # (7)
                                  echo "[RENAME] $2 -> $3"
                                  echo "> deletefile ftp:${FTP_SERVER_DIR}$2"
                                  rclone deletefile ftp:${FTP_SERVER_DIR}$2 $options

                                  echo "> copyto ./$3 ftp:${FTP_SERVER_DIR}$3"
                                  rclone copyto ./$3 ftp:${FTP_SERVER_DIR}$3 $options

                                  echo "> rmdir ftp:${FTP_SERVER_DIR}${2%/*}"
                                  rclone rmdir ftp:${FTP_SERVER_DIR}${2%/*} $options
                                  ;;
                                M)
                                  # (8)
                                  echo "[MODIFY] $2"
                                  echo "> copyto ./$2 ftp:${FTP_SERVER_DIR}$2"
                                  rclone copyto ./$2 ftp:${FTP_SERVER_DIR}$2 $options
                                  ;;
                                D)
                                  # (9)
                                  echo "[DELETE] $2"
                                  echo "> deletefile ftp:${FTP_SERVER_DIR}$2"
                                  rclone deletefile ftp:${FTP_SERVER_DIR}$2 $options

                                  echo "> rmdir ftp:${FTP_SERVER_DIR}${2%/*}"
                                  rclone rmdir ftp:${FTP_SERVER_DIR}${2%/*} $options
                                  ;;
                                A)
                                  # (10)
                                  echo "[ADD]    $2"
                                  echo "> copyto ./$2 ftp:${FTP_SERVER_DIR}$2"
                                  rclone copyto ./$2 ftp:${FTP_SERVER_DIR}$2 $options
                                  ;;
                              esac
                            done
                          }

解説

(1)

mainブランチへのプルリクエストが閉じられたときに開始します。

(2)

このプルリクエストがマージされたときに、ジョブを開始します。(1)の指定と合わせて、mainブランチにマージされ、プルリクエストが閉じられたときに、ジョブが開始することになります。

(3)

マージコミットをチェックアウトします。のちに、git diffコマンドで前回コミットとの差分を見るため、fetch-depth2に設定します。

(4)

マージコミットとその直前コミットとの差分を出力します。

--name-statusオプションで、変更されたファイル名と変更要因(追加、変更、名前変更、削除など)を出力します。たとえば、次のように出力されます。コメント部分は追記したものです。

                    A       file-a           # file-aが追加された(Added)
                    M       file-b           # file-bを変更(Modified)
                    R100    file-c   file-d  # file-cをfile-dに移動(Renamed). 内容は100%一致.
                    D       file-e           # file-eを削除(Deleted)

この標準出力を、パイプで後のシェルグループ{}につなげています。

(5)

標準入力から一行読み込み、line変数に格納します。

(6)

読み込んだ一行の文字列lineを空白文字で分割し、位置パラメータに格納します。具体例は以下の通りです。

                    line="R100    file-c   file-d"
                    set $line
                    echo $1
                    # R100
                    echo $2
                    # file-c
                    echo $3
                    # file-d
(7)

変更要因が名前変更(Rename)の場合、まずリモート側の変更前ファイルを削除し、次に変更後のファイルをリモートにアップロードします。

最後に、削除したファイルの親ディレクトリが空の場合は、削除します。

(8)

変更要因が内容変更(Modify)の場合、最新のファイルをリモートにアップロードします。

(9)

変更要因が削除(Delete)の場合、リモート側のファイルを削除します。

(10)

変更要因が追加(Add)の場合、追加されたファイルをリモートにアップロードします。

注釈

  1. ^ GitHubのサイト更新により設定場所が変わる可能性があります。

参考文献

  1. ^ "GitHub Actions".GitHub. Retrieved 2023-08-11.
  2. ^ "GitHub Actionsを使ったFTPの自動デプロイ". Qiita. Retrieved 2023-08-11.
  3. ^ "github actionsを用いたFTP自動デプロイ。(例:さくらサーバー)". Zenn. Retrieved 2023-08-11.
  4. ^ a b c "rclone". rclone.org. Retrieved 2023-08-09.
  5. ^ "rclone/docs/configure". rclone.org. Retrieved 2023-08-09.
  6. ^ "rclone/docs/environment-variables". rclone.org. Retrieved 2023-08-10.
  7. ^ a b c "FTPS". wikipedia. Retrieved 2023-08-10.
  8. ^ a b "Encrypted secrets". GitHub. Retrieved 2023-08-10.
  9. ^ a b "rclone/commands/sync". rclone.org. Retrieved 2023-08-10.
「https://www.contentsviewer.work/Master/software/ci-cd/ftp-with-rclone-on-github-actions/article」から取得