風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

RHEL5.1でFTPのダウンロードが正常に完了しない場合がある?

現象

仕事で使っている

Red Hat Enterprise Linux Server release 5.1 (Tikanga)

で、

wget ftp://user:password@ftp.hogehoge.com/fuga.tar

のようにして、約900MBのファイルをダウンロードしようとすると、

==> PASV ... 完了しました。 ==> RETR fuga.tar ... 完了しました。
長さ: 952033280 (908M)

100%[===================================================================================================>] 952,033,280 1.00M/s in 15m 8s

06:34:45 (1.00 MB/s) - 制御用の接続を切断します。
再試行しています。
:

みたいな感じになって、サイズ的には全てダウンロードしているにも関わらず、正常に完了しないことがあるみたい。
wgetのバージョンをGNU Wget 1.10.2 (Red Hat modified)→GNU Wget 1.11.4 Red Hat modifiedにしてもダメ。

さくらのレンタルサーバ上で同じことをやってみると、正常に完了するので、環境依存か……。
さくら上のはGNU Wget 1.12 built on freebsd7.1.だけれども。

困っているのは…

RHEL5.1上にて、Python 2.7でftplibを用いた簡単なツールを使っているんだけれども、

from ftplib import FTP_TLS
ftps = FTP_TLS('ftp.hogehoge.com')
ftps.login(user='user', passwd='password')
ftps.prot_p()
ftps.retrbinary(u'RETR fuga.tar', open('fuga.tar', 'wb').write) # ここでブロックされていつまでも返らない

みたいなことをやってみても、やっぱりサイズ的には全てダウンロードし終わっても、retrbinary()で停まったまま。
Ctrl+Cすると、

File "/usr/local/python/lib/python2.7/ftplib.py", line 694, in retrbinary
return self.voidresp()
File "/usr/local/python/lib/python2.7/ftplib.py", line 224, in voidresp
resp = self.getresp()
File "/usr/local/python/lib/python2.7/ftplib.py", line 210, in getresp
resp = self.getmultiline()
File "/usr/local/python/lib/python2.7/ftplib.py", line 196, in getmultiline
line = self.getline()
File "/usr/local/python/lib/python2.7/ftplib.py", line 183, in getline
line = self.file.readline()
File "/usr/local/python/lib/python2.7/socket.py", line 447, in readline
data = self._sock.recv(self._rbufsize)
File "/usr/local/python/lib/python2.7/ssl.py", line 232, in recv
return self.read(buflen)
File "/usr/local/python/lib/python2.7/ssl.py", line 151, in read
return self._sslobj.read(len)

みたいな感じなので、socket系のライブラリに問題があってブロックされてしまっているんだと思うが……。
ちなみに、さくら上(Python 2.7)で同一プログラムを走らせると、正常に完了する。

暫定対策

とりあえず、ツールのほうは、

finfo = dict(
  ftps = ftps,
  local_fp = open('fuga.tar', 'wb'),
  total_size = ftps.size('fuga.tar'),
  receive_size = 0,
)
def _write(data):
  finfo['receive_size'] += len(data)
  finfo['local_fp'].write(data)
  if finfo['total_size'] <= finfo['receive_size']:
    finfo['ftps'].abort()
try:
  ftps.retrbinary(u'RETR fuga.tar', _write)
except Exception, s:
  if finfo['total_size'] != finfo['receive_size']:
    print u'RETR Error %s' % (s)

のように、書き込みごとにサイズをチェックして、予め取得しておいたファイルサイズと同じになったらabort()でファイル転送中止してretrbinary()から抜ける、という仕組みを入れて対処してみたが、ちょっと気持ちが悪い。
SIZEコマンド未サポート/正確なサイズが返らないサーバだったら対応できないし…