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)
手動でワークフローを起動できるように設定。
- (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-depth
を2
に設定します。- (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)の場合、追加されたファイルをリモートにアップロードします。
注釈
- ^ GitHubのサイト更新により設定場所が変わる可能性があります。
参考文献
- ^ "GitHub Actions".GitHub. Retrieved 2023-08-11.
- ^ "GitHub Actionsを使ったFTPの自動デプロイ". Qiita. Retrieved 2023-08-11.
- ^ "github actionsを用いたFTP自動デプロイ。(例:さくらサーバー)". Zenn. Retrieved 2023-08-11.
- ^ a b c "rclone". rclone.org. Retrieved 2023-08-09.
- ^ "rclone/docs/configure". rclone.org. Retrieved 2023-08-09.
- ^ "rclone/docs/environment-variables". rclone.org. Retrieved 2023-08-10.
- ^ a b c "FTPS". wikipedia. Retrieved 2023-08-10.
- ^ a b "Encrypted secrets". GitHub. Retrieved 2023-08-10.
- ^ a b "rclone/commands/sync". rclone.org. Retrieved 2023-08-10.