WordPressプラグイン「BackWPup」で定期的にブログのバックアップを取っているのですが、もしもサーバー障害などでデータが飛んだらと考えるとバックアップの意味がありません。
また、長期障害発生中に他社サーバーに移転したい場合にもFTPの接続が出来なかったら、バックアップを取ったデータをダウンロードすることが出来ませんね。
なので、定期的にFTPでローカルにバックアップデータをダウンロードするのも良いですが、毎回やるのも面倒くさいしファイルが増えてきたら削除も大変。
そう考えていたら、ラズベリーパイがあるのを思い出し、バックアップデータを定期的にローカルに保存して、世代管理も出来るプログラムをpythonで作りました。
と言っても動作確認は「自分」の環境でしかしてないので、全ての人で動くか不明です。また、プログラムも「やりたい事の組み合わせ」なので非効率だったり汚いです。
ラズベリーパイを使う理由は、常時パソコンを稼働させるわけにはいかないのと、環境構築が面倒だから。Pythonはこのブログに公開してる管理人宅の室内温度表示で使ってるのもあって、ググって出てきたサンプルコードの組み合わせという非常に低レベルではあるものの、まだ慣れている部分がある。
2019年5月13日追記
この度、さくらインターネットのレンタルサーバーを借りて同じプログラムを実行したのですが、FTP接続でエラーが出たので修正しました。
それに伴い、ソースの修正と、今までのバージョン毎の修正内容も概要で書きました(過去バージョンはソース無し)。
FTPでファイルダウンロードする際のプログラムの課題
といってもバリバリとプログラムが書ける技術は無いので、ネット上にあるサンプルコードを参考に組み合わせて何とか動かすことにする。
プログラム実行環境
- Raspberry Pi2
- 有線LAN
- python3
多分、Raspberry Pi ZeroWHの無線LAN環境でも動くと思う。
やりたいこと
- FTP接続でサーバーからファイルダウンロードする
- ローカル(ラズパイ)に保存したファイルの世代管理をする
- ブログのURL一覧リストが書いてる外部ファイルからブログ毎のバックアップファイルをDL
FTPで接続してファイルのダウンロードが出来ればBackWPupプラグインで作成されたバックアップデータもダウンロードできる。また、ローカルの保存しているバックアップファイルの個数も分かれば世代管理も出来ると考えた。
さらに、テキストファイルなどで、サイトドメインやサーバー内のBKファイルの保存先パスを指定できれば、同一サーバー内の複数のWordPressサイトのBKデータもダウンロードできる。
FTPでファイルを取得しないと始まらない。BackWPupで定期実行してるバックアップファイル(以後BKファイル)は15日分の世代管理しているので、ローカルにダウンロードしたBKファイルも同じ日数分を世代管理する。
リトルサーバーでは複数サイトのWordPressを運用してるので、URLリストを書いたテキストファイルからリトルサーバー内のWordPress毎の「BackWPup」で作成されたバックアップデータをダウンロードする。FTPのアカウントは1つで行う。外部ファイルにURLリストを書く理由は、管理のしやすくするのと、WordPressのサイトの件数が変わっても対応出来るようにするため。
下は、FTPでダウンロードしたバックアップデータ保存先階層のイメージ。
/littleserver #レンタルサーバー毎にディレクトリ管理 |--wp_bk.py #pythonの実行ファイル |--URL_List.txt #サイトドメインとサーバー上のBKファイルの置いてあるディレクトリパス一覧 |--error.log #FTP再接続の上限超えて失敗したら記録 |--/coronblog.kanazawacycleparking.jp | |--bk-1.zip | |--bk-2.zip | : |--/example.com | |--bk-1.zip | |--bk-2.zip | : /sakura #レンタルサーバー毎にディレクトリ管理 |--wp_bk.py #pythonの実行ファイル |--URL_List.txt #サイトドメインとサーバー上のBKファイルの置いてあるディレクトリパス一覧 |--error.log #FTP再接続の上限超えて失敗したら記録 |--/example2.com | |--bk-1.zip | |--bk-2.zip | : |--/example2.net | |--bk-1.zip | |--bk-2.zip | :
レンタルサーバー毎にディレクトリを分けている。なので、FTPログイン情報を実行ファイル[wp_bk.py]毎に記載している。
[URL_List.txt]の中身は[ローカル保存先,サーバー上のBKファイル保存先ディレクトリのフルパス]
url_list.txt
example.com,/home/ユーザー名/www/example.com/wp-content/…
example.net,/home/ユーザー名/www/example.net/wp-content/… sub1.example.net,/home/ユーザー名/www/sub1.example.net/wp-content/…
サーバーによってドメインのディレクトリパスが変わってきますが、こんな感じのテキストファイルです。
同じレンタルサーバー内なら複数のWordPressサイトのバックアップデータもダウンロード可能。ローカルの保存先はドメインのディレクトリ名[example.com]で管理されてる(ローカルの保存先はURL以外にも変更可)。
なお、ローカルのディレクトリ階層を[example.com/blog1]のようには出来ない。
やりたいことからプログラム実行順を考える
- URLリストの外部ファイルからURLを取得して配列に入れる
- ローカルにあるバックアップデータのファイルリストを取得
- FTPでサーバーにあるバックアップデータのファイルリストを取得
- ローカルのファイル名とサーバーのファイル名を比較(照らし合わせる)
- ローカルに存在しないファイルのみサーバーからダウンロードする
- ローカルにあるファイル件数が世代管理の上限15件を超えたら古いファイルを削除する
- URLリスト分を繰り返す
このような流れ。
いきなり外部ファイルから取得したサイト件数分を書こうとすると頭がこんがらがるので、まずは1サイトだけ書いた外部ファイル取得、1バックアップファイルのダウンロード、そして1サイト内のBKファイル全件ダウロード、世代管理。
それが出来ると、サイト件数分繰り返すように処理すれば多分行けると思う。
なので、上記リストのプログラム処理を一つ一つ確認しながら作っていった。
プログラムの動作確認
一度ダウンロードしたBKファイルを古いファイルと見立てて名前を変更して、もう一度ダウンロード。名前を変更したBKファイルはサーバーには存在しないのでローカルでも削除される。
既にダウンロード済みのBKファイルも再びダウンロードしないようにもなっている。URLリストに新たにURLを追加したサイトのBKファイルもダウンロードできている(同一サーバー内に限り)。
全ソース
改めておさらい。
- 同一サーバー内のサイト毎のBKファイルをダウンロードする
- ローカルとサーバーのBKファイルの差分から世代管理する
- ダウンロード中に中断したらFTP再接続(2019/5/13追記)
ローカルに保存してるBKファイルの世代管理はファイル数で行う予定だったが、サーバーのBKファイルとの差分を取る方法に変更した。処理としてはサーバーのバックアップディレクトリに無いファイルを削除する。
事前に必要なPythonのパッケージをインストール。
wp_bk.py
#!/usr/bin/env /usr/bin/python
# -*- coding: utf-8 -*-
import re
import os
import glob
import ftplib
import time
from datetime import datetime
dir_path = 'ローカル保存先ディレクトリパス' #絶対パス
file_gen = 15 #BKファイルの世代管理の上限数
ftp_host = 'FTPホスト名'
ftp_pass = 'FTPパスワード'
ftp_user = 'FTPユーザー名'
retry_n = 3 #FTPダウンロード再接続の上限回数
#サーバーのBKファイル一覧取得関数
def ftp_cwd_list(dir_var):
# 接続先サーバーのホスト名
ftp = ftplib.FTP(ftp_host)
ftp.set_pasv("True")
# ユーザ名とパスワードを指定しログイン
ftp.login(ftp_user, ftp_pass)
ftp.cwd(dir_var)
#空のタプル
file_list_var = []
try:
file_list_var += ftp.nlst('*.zip')
except Exception as e:
print('FTP_zipファイル取得エラー')
try:
file_list_var += ftp.nlst('*.gz')
except Exception as e:
print('FTP_gzファイル取得エラー')
ftp.close()
time.sleep(2) #時間待ち。この処理は不要かも
return file_list_var
#BKファイルダウンロード関数
def ftp_file_dl(local_dir_var, ftp_dir_var, file_name):
#ファイルDL失敗したら再接続。上限回数=retry_n
for cnt in range(retry_n):
try:
print('FTP接続(' + str(cnt+1) + ')回')
print(datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
# 接続先サーバーのホスト名
ftp = ftplib.FTP(ftp_host)
ftp.set_pasv("True")
# ユーザ名とパスワードを指定しログイン
ftp.login(ftp_user, ftp_pass)
ftp.cwd(ftp_dir_var) #ディレクトリ移動
with open(local_dir_var + '/' + file_name, 'wb') as f:
ftp.retrbinary('RETR %s' % './' + file_name, f.write)
print('ダウンロード完了')
ftp.close()
break #ダウンロード完了したらFTP再接続ループから脱出
except Exception as e:
if os.path.exists(local_dir_var + '/' + file_name):
os.remove(local_dir_var + '/' + file_name)
print('FTP_ファイルDL失敗')
if cnt + 1 >= retry_n : #FTP上限回数超えて切断したらログを記録する
print('ログ記録') #[日時、ディレクトリ名/ファイル名]
f = open('error.log' , "a")
f.write(datetime.now().strftime("%Y/%m/%d %H:%M:%S") + ',' + local_dir_var + '/' + file_name + "\r\n")
f.close()
time.sleep(2) #時間待ち。この処理は不要かも
#プログラム開始
os.chdir(dir_path) #ディレクトリ移動
#ログデータ作成(空のファイル作成)
if not os.path.exists('error.log'):
f = open('error.log','w')
f.write('')
f.close()
#サイトリストを取得(外部ファイル)
with open('url_list.txt', 'r') as f:
url_list = f.read()
url_list = re.split('[\n,]', url_list)
url_cnt = len(url_list)
#サイト毎に処理開始
for i in range(0,url_cnt,2): #1つ飛び出ループ
print(url_list[i])
#サイト毎にディレクトリ作成
if not os.path.isdir(url_list[i]):
os.mkdir(url_list[i])
ftp_file_name = ftp_cwd_list(url_list[i+1])
local_dir_list = os.listdir(url_list[i])
local_dir_list.sort()
local_name_set = set(local_dir_list)
ftp_name_set = set(ftp_file_name)
new_file_name = list(ftp_name_set -local_name_set) #サーバにある新しいファイル取得
#リストを昇順にソート
new_file_name.sort()
if len(new_file_name) != 0:
for new_file_name in new_file_name:
print('ダウンロード:' + new_file_name)
ftp_file_dl(url_list[i], url_list[i+1], new_file_name)
else: #新しくダウンロードするファイル無し
print('新規BKファイル無し')
#ここからBKファイルの世代管理
local_dir_list = os.listdir(url_list[i])
local_dir_list.sort()
print(len(local_dir_list) > file_gen)
if len(local_dir_list) > file_gen:
file_cnt = len(local_dir_list) - file_gen
for n in range(file_cnt):
print('ファイル削除:' + local_dir_list[n])
os.remove(url_list[i] + '/' + local_dir_list[n])
else:
print('削除ファイルなし 世代:' + str(len(local_name_set)))
FTPでBKファイルダウンロード中は例外処理していませんが、とりあえず動作したので一応完成。
後は、「BackWPup」でバックアプ取った1~2時間後ぐらいに、ラズベリーパイのcronで定期的に実行するだけです。
Pythonのプログラムの課題で調べたこと
ここからは、動作確認で調べて役に立ったサイトをリンクしておく。
改めて課題
- ローカルのディレクトリ内のファイル一覧を取得
- ファイル取得を更新日(昇順)で出来るか
- FTPでサーバーのディレクトリ内のファイル一覧の取得
- FTPで指定したファイルをダウンロード
- 外部ファイルを読み込んで配列化する
- ローカルのファイル操作(削除)
- foreachの使い方
特に太字が自分の中で理解が不十分なので重要と考える。
コード記載している部分もありますが、初期に作成したプログラムの内容なので、今と若干違う書き方なところもあります。
ローカルのディレクトリ内のファイル一覧を取得
ローカルのディレクトリ内のファイルをリストで取得する。リストで取得できれば、ローカルとサーバ側のBKファイルの「照らし合わせ」が出来るようになる。
ディレクトリ内のファイルのリストを取得するにはここで確認
「glob.glob()」を使うとフォルダまでパスに入ってたので、この中の「os.listdir()」を使用。ワイルド―カードが使えないので全てのファイルを取得してしまいますが、ディレクトリ内をBKファイルだけにすれば問題ないでしょう。
ファイル取得を更新日(昇順)で出来るか
[os.listdir()]の並び順を調べたら、どうやら不定らしい。
次にリストのソートについて調べる。
[.sort()]を使えば昇順になるらしい。つまり、一番古いファイル名がリスト[0]になる。これで、世代管理で古いファイル削除するのとサーバにあるBKファイルとの比較が出来るようになった。
一から配列の並び替えを考えて作らなくて済んだのでかなり楽。
FTPでサーバーのディレクトリ内のファイル一覧の取得
FTPの接続は以前書いたプログラムからコピペ。
サーバーのディレクトリ内のファイル一覧の取得はこちらを参考。
FTPのファイルリスト取得は関数にした[def ftp_cwd_list(dir_var)]
この関数でFTP接続とファイル一覧を取得している。変数[dir_var]にはサーバーのBKファイル保存先絶対パスが入る。
取得したファイルリストを確認したら昇順されているようだが、一応は[.sort()]で昇順に。
ログイン(接続)・ファイル一覧取得・ファイルダウンロード・切断を分けて使おうとしたが「ftp」が関数の外で使ってないから関数分けると定義されてないなんとかでエラーが出た。まあグローバル変数で「ftp = ftplib.FTP(“ホスト名”)」を定義すればいいんだけど、書くのが面倒なので1回1回接続切断することにした。
FTPでファイルをダウンロード
ファイルのダウンロードはFTPのファイル一覧取得と同じサイトを参考。
失敗談
ファイルをダウンロードしたら、そのまま保存されると思っていた。ローカルディレクトリにBKファイル作成されたから、てっきりダウンロードしたかと思ったけどファイルの中身が空(0KB)。処理もタイムアウトになっていた。
(今思い返すと自分でも何を言ってるか分からない。多分、FTPからダウンロード後に「ファイル書き込み」をしてなかったのだと思う。自分で書いておきながらどんな失敗だったか忘れてるのは恥ずかしい 2019/5/13)
なので、FTPからダウンロードしたら、そのファイル名で保存してくれるわけでは無いようだ。
ローカルのディレクトリに「ファイル作成」して、そのファイルにダウンロードしたファイルを保存するみたい。
[def ftp_file_dl(local_dir_var, ftp_dir_var, file_name):]で関数作成。
- local_dir_var(ローカルの保存先パス)
- ftp_dir_var(サーバーからダウンロードするファイルのディレクトリパス)
- file_name(BKファイル名)
外部ファイルを読み込んでリスト化する
外部ファイルにはURLとサーバ側のBKファイル保存先のフルパスを書き込んでいる。
外部ファイルを読み込むのと「改行」で分割する方法はこちらを参考にした。
確認用コード
with open('url_list.txt', 'r') as f: url_list = f.read() url_list = url_list.split('\n') #改行で分割 print(url_list) print(len(url_list))
結果
['url1,パス1', 'url2,パス2']
面倒くさいけど、一つ一つ出力結果を確認しながら進めていった。
改行で分割してリスト化出来た。ただ、URLとパスも分割しないと行けない。
for文で「url_list[n]」を配列のようにして、さらに「split」でURLとパスの「,」を分割しようとしたけどエラーでうまくいかない。
さらに調べると[split]メソッドで複数分割するには[re]モジュールを使えば良い事が分かった。
確認用コード
import re with open('url_list.txt', 'r') as f: url_list = f.read() url_list = re.split('[\n,]', url_list) #改行とカンマで分割 print(url_list) print(len(url_list))
結果
['url1', 'パス1', 'url2', 'パス2']
「リストの偶数 = URL、リストの奇数 = サーバ側バックアップのパス」になったので、とりあえずURLとパスで分割できるようになった。
ちなみに[url_list.txt]の最後が改行で終わってると「区切り文字の分割」でエラーになってプログラムが中断される。
foreachの使い方
ローカルとサーバーにあるBKファイルで同一名の重複チェックは「繰り返し」になるのでfor文が必要だ。その中でサーバーにしか存在しないBKファイルをリストに追加する。ダウンロードするBKファイル名をリスト化することでローカルに存在しないファイルだけ、サーバーからBKファイルをダウンロードできるようになる。
ただ、Pythonには「foreach」は無いようだ。なので、for文を使う。まずはFTPで取得したファイル一覧を抽出する。
サーバーにあるローカルに存在しないBKファイルを全てダウンロードする
まずは、for文でファイル名抽出するの動作確認。
for file_name in ftp_file_name: print(file_name)
意外と簡単だった。なるほど、「ftp_file_name」変数のリストを1つ1つ取り出して「file_name」の変数に入れている。
C言語にあるような「print(file_name[i])」みたいな記述は不要。リストが全て抽出するとループから抜けるもよう。
これで取得したファイル一覧や外部ファイルのURLの個数が変則的でも対応できそう。ただ、ローカルとサーバーにあるBKファイルの照らし合わせは出来なさそうなので考えてみる。
やりたい処理はこれ
- ローカルBK0:サーバーなし
- ローカルBK1:サーバーBK1
- ローカルBK2:サーバーBK2
- ローカルなし:サーバーBK3
両者のBKファイルを比較してサーバー側にしかないBKファイル(この場合はBK3)のみダウンロードしたい。
forで繰り返してif文で両者のリストを比較しようと思ったが、このサイトでも書いてあるように簡単にリストの比較が出来ることが分かった。
ただ、比較して共通しないBKファイルを抽出したいので、「みんなのPython」を参考に差分で抽出する。
local_name_set = set(local_dir_list) #ローカルのBKファイルのリスト ftp_name_set = set(ftp_file_name) #サーバーのBKファイルのリスト new_file_name = list(ftp_name_set -local_name_set) #サーバにしか存在しないファイル取得 old_file_name = list(local_name_set - ftp_name_set) #ローカルにしか存在しないファイル取得 new_file_name.sort() old_file_name.sort()
それぞれ、「ローカルにしか存在しないBKファイル」、「サーバーにしか存在しないBKファイル」を比較している。リストの並び順が不定だったのでソートで昇順にしてる。
2つ用意することで、「サーバーからダウンロードするBKファイル」、「ローカルで削除する古いBKファイル」が出来ることになった。
これを利用して、先ほど作ったダウンロード関数からローカルに存在しないBKファイルを全てダウンロードするようにする。
if len(new_file_name) != 0: #リストが空ではない for new_file_name in new_file_name: #ローカルに存在しないBKファイルを全てDL ftp_file_dl(url_list[0], url_list[1], new_file_name) #FTPでダウンロード
このように追加した。「url_list[]」はそれぞれ、サイトURLとBKファイルのフルパス。まだ、1サイトでの動作確認。
サイト毎に繰り返す
先ほどループでローカルに存在しないBKファイルを全てダウンロードする処理を作りましたが、それを複数サイトでダウンロードするようにしました。
url_cnt = len(url_list) #サイトの件数 for i in range(0,url_cnt,2): #1つ飛び出ループ if not os.path.isdir(url_list[i]): os.mkdir(url_list[i]) #サイト毎にBKフォルダ作成 :
読み込んだサイト一覧の外部ファイルのリストの件数分繰り返します。ただし、リストは「URL,BKファイルのパス,URL,BKファイルのパス,…」になてるのでループは1つ飛びにしてます。
ついでにローカルのサイト別のフォルダ作成。
フォルダの作成と存在の確認はこちらを参考
サイト毎のループに今まで作った処理(FTPでBKファイルダウンロード)を行います。確認用で1サイトだけだったのを複数サイト対応にも修正。
ローカルのファイル操作(削除)
最後に、BKファイルのダウンロードが終わったら古いBKファイルを削除します。ローカルのファイルリストとサーバーのファイルリストの差分から「ローカルに存在していてサーバーに存在しないBKファイル」を削除します。
これで、サーバーにあるBKファイルとローカルにあるBKファイルの同期が取れるはずです。
for old_file_name in old_file_name: print('ダウンロード:' + old_file_name) os.remove(url_list[i] + '/' + old_file_name)
ローカルにしか存在しないBKファイル「old_file_name」を削除します。サーバーに無いファイルをローカルで全て削除してますが、サーバーのBKファイルのディレクトリと同じファイルにすることで、ローカルのBKファイルを世代管理しています。
サーバー側のBKファイルの世代管理はWordPressプラグイン「BackWPup」で管理してます。
ファイルの削除はこちら参考
過去バージョンのプログラムのバグなど
ちょこっと修正して動作確認の繰り返しなので、特にプログラムのバージョン管理はしてないのですが、修正していくうちに色々問題あったなあと思い返すと何かの役に立つかもしれないので、ここにバグの内容を書き残します。
事柄だけでソースはありません。
初代(バージョン1)
- バックアップデータの世代管理が出来ていなかった
ラズパイ本体にダウンロードしたバックアップデータの世代管理の方法がミスってた。
BackWPupプラグインで世代管理してたから同じにしようと「サーバー内にあるバックアップデータのファイル名と同じにしよう」と作っていた。
ファイル名で比較して
[サーバー内のバックアップデータ] ≠ [ローカル内のバックアップデータ]
にしてた。
それで動かしていたのですが、ある時もしや?と思ってサーバー内のバックアップデータを他のディレクトリに移してプログラムを走らせる。
案の定ローカル内のバックアップデータも全て消えた。世代管理していたと思っていたのが方法が間違っていた。
なので、プログラム修正。バージョン2に移る
バージョン2(2018/11/26修正)
バージョン2での修正部分
- 世代管理の方法を変更
初代がアレだったので、世代管理をローカルに保存してるファイル数をカウントして上限超えたらファイル名が古い(ファイル名に日付が入っているので)のをオーバーした分だけ削除した。
バージョン2に修正してから、かなりの月日動かしていた。ローカルでバックアップデータの世代管理が出来てるしこれと言って問題なし。レンタルサーバー変更してもFTPログイン情報とディレクトリパスさえ変更すれば動くだろう。
そう考えていた。
所が、新規に契約した(さくらインターネットの)レンタルサーバーで問題が発覚。今までのレンタルサーバーとFTPコマンドの挙動が変わっていた。
バージョン2での問題
- ファイルが無い拡張子だと450コード返ってエラーに
- バックアップデータダウンロード中に切断されることがある。426コードが返ってくる
他社のレンタルサーバー借りて始めた分かったバグ。これは例外処理とFTPの再接続で対応
バージョン3(2019/5/13)
バージョン3での修正部分
- [.zip] or [.tar.gz]のファイルリスト取得で空の時に返す450コードに例外処理で対応
- ダウンロード中に切断され436コードに例外処理で再接続させた(再接続の上限指定)
- 再接続の上限回数に達したらエラーログを記録するようにした
BackWPupのバックアップファイルをzipやtar.gzに設定できるからサイトによっては混在してた。なので両方取得するようにしていたのだけど、さくらインターネットのレンタルサーバーだとファイルが存在しない時450コードを返すみたいで、プログラムのエラーになりそこで処理が中断してた。
なので、例外処理で続けるようにした。
他にいい方法あるかもしれないですが、思いつく方法でこれが手っ取り早い(というか例外処理しか思いつかなかった)。
ダウンロード中のエラーも同様に、FTPに再接続して再ダウンロードするようにした。途中で中断したファイルを「途中から」ダウンロードする方法は知らないので、一度リムーブ(削除)して初めから再ダウンロードするようにした。
これで、1度失敗してもダウンロード完了する確率は上がった(意図的にダウンロードを中断させる以外で、今のところエラーログは出ずに2,3回の再接続でダウンロードが完了してる)
ただ、エラーログのファイルは、そんなにエラーになること無いと思うので日付で管理はしてない。1日1回エラーログを記録したとしても1年で365行で済む。
またバグが見つかったらこちらに追記します。
反省点
実は、半年以上前にPython入門書「みんなのPython」を購入している。ただ、買っただけでほとんど読んでいない。
購入の理由は、Pythonで文字コードを扱うのがややこしかったため。色々な入門書の中から文字コードの扱ってるページ数が多いこれにした。
今回のプログラム作る際に、ググって色々なサイトを参考にしたが、FTP以外の「リストの昇順、リストの比較(差分)、ファイル・フォルダの操作」などほとんどが、この本1冊に書いてある内容だった。
なので、見よう見まねで作ったものの、本1冊勉強していればもう少しすんなりと作れたのではと思う。基礎が大事だと改めて感じたので、今後はちゃんと本1冊勉強していきたい。
しかし2019/5/13プログラム修正で、この記事を手直ししていたら、当時言っていたことが「何を言っていたのか」分からない部分もある。
自分の勉強不足もあるのだが、もう少し具体的に書かないと当時遭遇した問題が「何だったか分からなくなる」のも反省。
あれから、何も変わってない・・・
2023/9追記:スクリプト(プログラム)改修
数年ぶりにプログラム改修。動作的な部分は変わってないですが、FTP接続情報を外部ファイルから取得したり、SFTPのパスワード認証にも対応させた。
あくまで自分用プログラムソースコードのバックアップメモ書きですが…