この記事の結論
curlのFTPSアップロードで0バイトになるなら、Pythonのftplibに替えて、置いた後にサイズを照合する形が確実です。僕の会社では curl -T で本番フォームの処理ファイルが0バイトになり、約3分止まりました。
目次6項目
curlのFTPSアップロードで0バイトになったとき、僕の会社で効いたのは原因の深追いではなく、Pythonのftplibに替えて、置いた後にサイズを照合することでした。2026年7月には、curl -T で本番のフォームの処理ファイルが0バイトになり、広告を出していたLPのフォームが約3分止まっています。
終了コード18の意味と、ftplibで置き直す手順、事故を見つける確認の仕方を順に書きました。「コマンドは終わったのに、サーバーのファイルが空」という方は、まず次の章の表から見てください。
curlのFTPSアップロードで0バイトになるとき:終了コード18と「空のファイル」の2つの出方
curlのFTPSアップロードの失敗には、僕の会社の記録では2つの出方がありました。途中で止まって終了コード18を返す形と、サーバー上のファイルが0バイトになる形です。
FTPSとは、ファイルを送るFTPの通信を、SSL/TLSで暗号化して行う方式のことです。レンタルサーバーへファイルを置くときによく使われ、curlでもPythonでも扱えます。
終了コードとは、コマンドが終わったときに返す、成功か失敗かを表す番号のことです。curlの公式のマニュアルでは、0以外の番号ごとに失敗の種類が決まっています。
僕の会社で出た18は、公式のマニュアルで「Partial file」とされています。ファイルの一部しか送れなかった、という意味です。libcurlのエラー一覧でも、転送が想定より短いか長かった、と説明されています。
FTPのアップロードで近い番号を、公式の説明と並べます。どれが出たかで、次に見る場所が変わります。
| curlの終了コード | 公式の説明(要約) | アップロードで見ること |
|---|---|---|
| 18 | ファイルの一部しか送れなかった | サーバー上のファイルが途中までか空になっていないか |
| 25 | アップロードを始められなかった。FTPではサーバーがSTORを拒んだことが多い | 置き先のフォルダの権限・空き容量 |
| 64 | 求めたFTPのSSLのレベルを満たせなかった | サーバーがFTPSに対応しているか・設定 |
| 9 | ログインかフォルダへのアクセスを拒まれた | アカウントと置き先のパス |
僕の会社で困ったのは、18よりも「空のファイル」の方でした。番号が出れば失敗に気づけますが、空のファイルはサーバー上に残り、そのまま本番で使われます。
なぜ0バイトになったのか、通信のどこで欠けたのかまでは、僕の会社では突き止めていません。記録にあるのは、curlでは失敗し、Pythonのftplibでは同じサーバーに置けた、という事実です。この記事では、その範囲で書きます。
curlの0バイト事故の対処手順:Pythonのftplibで置き直し、サイズを照合する
本番のファイルが0バイトになったら、まず手元の正しいファイルを置き直してサービスを戻します。そのうえで、アップロードの手段をPythonのftplibに替え、置くたびにサイズを照合します。
- 本番のURLを開き、止まっているかを確かめる。 フォームの処理ファイルなら、送信が通るかを見ます。あわせて、サーバー上のサイズが0になっていないかを確かめます。
- 手元の正しいファイルを、ftplibで置き直す。 下のコードの形です。curlで置き直すと、同じ事故を重ねる恐れがあります。
- サーバー上のサイズと手元のサイズが同じかを確かめる。 違えば、もう一度置きます。
- 本番のURLのHTTPステータスを確かめる。 ページなら200が返るかを見ます。
- 以後のアップロードを、同じ手順のスクリプトに替える。 curlは、ダウンロードや一覧の取得だけに使います。
Pythonのftplibで、FTPSで1ファイルを置く形は次のとおりです。ホスト名・ユーザー名・フォルダ・ファイル名は例です。
import ftplib, os, ssl
host = "ftp.example.com"
user = "user"
password = os.environ["FTP_PASSWORD"] # パスワードはコードに直接書かない
name = "index.html"
ctx = ssl.create_default_context()
ftp = ftplib.FTP_TLS(host, context=ctx, timeout=60)
ftp.login(user, password)
ftp.prot_p() # データの接続も暗号化する
ftp.cwd("public_html")
with open(name, "rb") as fh:
ftp.storbinary("STOR " + name, fh)
remote = ftp.size(name)
local = os.path.getsize(name)
ftp.quit()
if remote != local:
raise SystemExit(f"サイズ不一致: local={local} remote={remote}")
print("OK", local)
FTP_TLS は、FTPにTLSを足すPythonの標準の部品です。Pythonの公式ドキュメントには、データの接続は利用者が prot_p() を呼んで明示的に守る必要がある、と注意が書かれています。
storbinary() は、ファイルをバイナリのまま送る命令です。公式ドキュメントでは、渡すファイルはバイナリで開いたものとされているので、"rb"(バイナリで読む)で開きます。
size() は、サーバー上のファイルの大きさを聞く命令です。公式ドキュメントでは、この SIZE という命令は標準化されておらず、多くのサーバーが対応している、という扱いです。
返り値が None なら、照合できなかったものとして失敗に数えます。
裏方で実際に起きた例:フォームの処理ファイルが0バイトになり、約3分止まった2026年7月
ここからは、僕の会社(株式会社UrakaTA)の運用記録からの転記です。お客さんの案件なので、サイト・サーバー・ファイルの場所が分かる情報は伏せています。
2026年7月29日、広告を出していたLPのフォームのファイルを、本番のサーバーへ反映しようとしました。サーバーはレンタルサーバーで、置き方はFTPSです。
反映には、curl -T にFTPSのオプションを付けたスクリプトを使いました。
この反映が失敗し、フォームの送信を受けるPHPのファイルが、本番で0バイトになりました。フォームの送信を受けるファイルが空なので、問い合わせを受けられない状態です。止まっていたのは約3分でした。
記録には、問い合わせを取りこぼす一歩手前だった、と残っています。広告から人が来ている最中のフォームだったからです。
まずかったのは、前日の7月28日にも、別のファイル(ページのHTML)の curl -T で同じ終了コード18を出していました。それなのに、手順をスクリプトにするとき、curlを使ってしまいました。
この件のあと、僕の会社では本番へのアップロードをPythonのftplib(FTP_TLS と prot_p())に限っています。置いた後には、サイズの照合と、本番URLのHTTPステータスの確認を必ず行います。
2026年8月にこのコラムを初めて公開したときも、アップロードはftplibで行いました。置いた21ファイルすべてで、手元とサーバーのサイズが一致することを確かめています。
FTPSアップロードの0バイトを再発させない:サイズ照合とHTTPの確認をスクリプトに入れる
再発を防ぐ要は、人の注意ではなく、スクリプトが自分で失敗を止める形にします。僕の会社では、次の3つを全部のアップロードの決まりにしています。
- アップロードはftplibだけにする。 curlは、ダウンロード・ファイルの削除・一覧の取得には使ってよい、という線を引いています。
- 置いた後に、サイズとHTTPの両方を確かめる。 どちらかが合わなければ、スクリプトを異常終了させます。
- 動いている本番のファイルは、触る前にバックアップを取る。 空のファイルが残っても、すぐ戻せるようにするためです。
HTTPの確認で何を「正常」と見るかは、ファイルごとに決めておきます。僕の会社のフォームの処理ファイルは、ブラウザで開く(GET)と405を返す作りです。そのため405なら正常、ページなら200を正常としています。
| 確認 | 合格 | 不合格のときにすること |
|---|---|---|
| サイズの照合 | サーバー上のサイズが手元と同じ | もう一度置く。size() が None なら照合できていないので失敗に数える |
| HTTPステータス | ページは200・フォームの処理ファイルは作りどおりの番号 | 手元のファイルとバックアップで戻す |
| 終了の仕方 | 2つとも合格のときだけ正常終了 | 異常終了させ、後の処理(通知など)に進ませない |
サイズの照合は、空のファイルを見つけるためのものです。終了コードが0でも、サーバー上のファイルが手元と違えば、それは成功ではありません。
この決まりは、手順書にも書いて残しています。たとえば僕の会社のお知らせ記事の更新の手順書には、本番への反映はftplibだけ、curl -Tは0バイト事故の実績あり、と書いてあります。
業務でFTPSのアップロードをスクリプトやAIに任せるときの注意
一度起きた事故は、手順書に書いてから次の作業に入る。 僕の会社の例では、前日に同じ失敗をしていたのに、手順をスクリプトにするときに繰り返しました。覚えているつもりでも、作り直す人やAIには伝わりません。
curlをFTPSで使うなら、--ssl ではなく --ssl-reqd を付ける。 curlの公式のマニュアルでは、--ssl は暗号化に対応しないサーバーだと暗号化なしの接続に戻ります。
--ftp-ssl はこの --ssl の旧名です。--ssl-reqd は、暗号化できなければ接続を打ち切ります。
パスワードをスクリプトに直接書かない。 上のコードのように、環境変数や、共有しない設定ファイルから読み込みます。スクリプトは手順書と一緒に共有されやすく、パスワードも一緒に広がるからです。
お客さんのサーバーでは、止まった時間と戻した時刻を記録して報告する。 数分でも、問い合わせのフォームが止まれば取りこぼしが出ることがあります。何が・いつからいつまで止まったかを残し、持ち主に伝えます。
置いたファイルが正しくても、サーバーの設定しだいで表示が変わることがあります。PHPのソースがそのまま表示された例は、.htaccess の SetHandler で PHP のソースが表示される原因に書きました。
管理画面から保存できずにFTPで置いた例は、WordPressの保存が403になりFTPで回避した手順にあります。
「コマンドは通ったのに本番が空になった」「アップロードの手順を安全に作り直したい」というときは、下のフォームからご相談ください。今の手順の確認から一緒に見ます。
よくある質問
curlの終了コード18は何を意味しますか?
ファイルの一部しか送れなかったという意味です。curlの公式のマニュアルでは「Partial file」とされています。アップロードでこれが出たら、サーバー上のファイルは途中までか空の可能性があるので、サイズを手元と比べてから次の作業に進みます。
curlの終了コードが0なら、アップロードは成功していますか?
終了コードだけでは判断しないのが安全です。僕の会社の記録では、curl -T のあとに本番のファイルが0バイトになっていました。置いた後にサーバー上のサイズを手元と比べ、ページのURLのHTTPステータスも確かめて、初めて成功と数えています。
curlをFTPSで使い続けたい場合、何に気をつければいいですか?
少なくとも --ssl ではなく --ssl-reqd を使います。curlの公式のマニュアルでは、--ssl(旧名 --ftp-ssl)はサーバーが対応していないと暗号化なしの接続に戻ります。そのうえで、置いた後のサイズの照合を必ず手順に入れてください。
Pythonのftplibで、ファイルの中身まで暗号化されますか?
FTP_TLS を使い、prot_p() を呼べば暗号化されます。Pythonの公式ドキュメントには、データの接続は利用者が prot_p() を呼んで明示的に守る必要がある、と書かれています。呼び忘れると、ログインは暗号化されても中身は暗号化されません。