[Miyadaiku] FTP サーバへ 記事 をアップロードする
2018-03-22

最近だと静的ブログというのは Github Pages にのせるのが普通なのかもしれませんが 私の場合はレンサバ的なものがあってそれが手持ち豚さんになってるのでそっちを使います。

私の使っているサーバは FTP/FTPS にしか対応していないし SITE コマンド のようにOSコマンドを実行する機能もないので、 アップロードするためのプログラムを Python で書きました。

コード

Note

Python3.6 で動作確認してます。

私は以下を deploy.py という名前で 直下 (outputs/ と同じ階層) に保存しました。

#!/usr/bin/env python3
# coding: utf-8

from ftplib import FTP, FTP_TLS, error_perm
import argparse
import getpass
import glob
import os
import json

parser = argparse.ArgumentParser('uploading files')

parser.add_argument('--glob', '-g', default='*', help='アップロード対象をGLOBで指定')
parser.add_argument('--onto', '-o', default='/', help='アップロード先のディレクトリ')
parser.add_argument('--plain', action='store_false', help='指定すると暗号化しない。')
parser.add_argument('--outputs', '-O', default='outputs/', help='outputs/ ディレクトリのパス')
args = parser.parse_args()

top = os.path.dirname(args.outputs.rstrip('/'))
try:
    info = json.loads(open(os.path.join(top, 'deploy.json')).read())
except FileNotFoundError:
    info = {}

if not info.get('user'):
    info['user'] = input('User: ')
if not info.get('passwd'):
    info['passwd'] = getpass()
if not info.get('host'):
    info['host'] = input('Host: ')


def make_hierarchical_paths(path):
    dirs = path.strip('/').split('/')
    root = '/' if path.startswith('/') else ''
    return [
        os.path.join(root, *dirs[0:i+1])
        for i, dir in enumerate(dirs)
    ]

ignore_errors = info.pop('ignore_errors', [])

cli = (FTP if args.plain else FTP_TLS)(**info)
try:
    cli.login()
except error_perm as e:
    if str(e) not in ignore_errors:
        raise e

os.chdir(args.outputs)
created = set()

for parent, _, _ in os.walk('.', topdown=False):
    for path in glob.glob(os.path.join(parent, args.glob)):
        for dirpath in make_hierarchical_paths(os.path.dirname(path)):
            if dirpath in created or not dirpath.strip('.'):
                continue
            print('making dir:', dirpath)
            try:
                cli.mkd(os.path.join(args.onto, dirpath))
            except error_perm:
                pass
            created.add(dirpath)

        if os.path.isdir(path):
            continue
        with open(path, 'rb') as f:
            print('uploading content:', path)
            cli.storbinary("STOR " + os.path.join(args.onto, path), f)

Warning

  • インデックスページが後になるように深い階層のファイルからアップロードされるようにしていますが、 環境によると思うので使う方は注意してください。

  • --plain オプションは使ってないのでうまく動かなかったらごめんなさい m(_ _)m

解説

基本的にやることは単純明快で Miyadaiku が出力してくれた outputs/ をまるっと対象の FTP サーバに転送しています。

いくつかオプションを用意しました。

glob

対象ファイルを限定するためのオプションです。 例えば html ファイルだけをアップロードしたい場合は --glob *.html のように指定します。 指定しないとすべてが対象となります。

onto

ファイルを配置するサーバ側のディレクトリを指定します。 指定しないと / に配置されます。

plain

指定すると SSL による暗号化を行いません。 サーバが FTPS に対応していない場合は 指定の必要があります。 指定しない場合は FTPS による接続を試みます。

outputs

outputs ディレクトリ の パス を指定します。 指定しない場合、カレントディレクトリにある outputs が対象になります。

また、 FTP を使うためにはサーバや認証情報が必要ですが毎回入力したくないので deploy.json がある場合はそこに書かれた接続情報を使うようにしています。

{
  "user": "ユーザ名",
  "passwd": "パスワード",
  "host": "FTPのホストアドレス",
  "ignore_errors": ["ログインした時に無視するメッセージ"]
}

user, passwd, host は指定しないと 実行時に入力を求められます。

ignore_errors ってなんだよって感じですが 私の場合は ログインした場合に毎回 503 You are already logged in って出てエラーになって進めない(謎)ので作りました。 環境によって発生する謎のエラー回避用です。

参考