manekineko倉金家ホームページ

趣味の部屋/ホームページ余話

ダウンロード専用フォルダというのをつくってみた

2013年2月1日 2014年2月20日 更新
 ファイルをダウンロードで提供したいことが時折あります。個別に強制ダウンロードを設定するのはめんどうなのでダウンロード専用フォルダというのを作ってみました。
この中にファイルを置くかリンクするだけでクリック一発!、そのファイルを強制ダウンロードさせることができます。


 ファイルをダウンロードで提供したいということが時折ある。
写真などの画像ファイルについてはいちいち面倒をみなくてもブラウザの右クリックで「画像(あるいはリンク先)をファイルに保存する」などで簡単にできるが、プログラムファイルについてはそうはいかない。
 ダウンロードが容易かどうかというだけの問題ではなく、phpやperlなどのcgiプログラムファイルについてはダウンロード以前に実行されてしまい、その実行結果しか保存されない。場合によってはダウンロードだけさせてそのファイルを実行させたくない場合もあり、その場合強制ダウンロードを設定しなくてはならないが、都度.htaccessなどを編集して設定するのもこれまためんどうだし、そのファイルを表示と兼用している場合はそれもできない。
 そのためファイルを.zipや.tar、.tgzなどでアーカイブしておくことになるのだが、やっぱりこれもめんどう。(不精の極みと言われれば決して反論はいたしませんが。)

 そこでダウンロード専用フォルダというのを設置して、その中のファイルはとにかく強制ダウンロードさせるようにしてみた。
 実はホームページを担当している同期会のサイトには数千の写真や資料があり、それのダウンロード用にはWebShareというHTTPで接続できるファイルブラウザを導入しているが、それほどでもなく数十程度のファイルならダウンロード専用フォルダで充分だろうと思った。

ダウンロード専用フォルダをつくる
さっそくダウンロード専用フォルダを作りましょう。
名前はわかりやすいように "download" としましょう。
ダウンロード用スクリプトを書く
ファイル名を表示してクリックでダウンロードするようphpスクリプトを書きましょう。
名前を "index.php" としてこのフォルダに置きましょう。

...とたったこれだけ。
→ ダウンロード専用フォルダ:名付けて「ダウンロードの小部屋」

 使い方は簡単。単にダウンロードさせたいファイルをここに置くかリンクするだけ。
さらにフォルダに分けて入れてもいいしフォルダをリンクしてもいい。
 もちろん上記ダウンロード専用スクリプトもここから生で(アーカイブされずに)ダウンロードできます。他にお薦めのがいくつか置いてあります。
気が向いたらどうぞ覗いてみてください。
(表示はapacheのインデックス風にしてみました。)
ちょっと欠点は今のところまだダウンロードの中断、再開には対応していません。
→いちおう対応してみました。動作確認が充分とは言えませんが... (2013年2月3日)



 とまあ今回はこんな方法で落ち着いたのだが、途中検討したことも参考として記載しておこう。
ただし以下はHTTPサーバーはApache/2.2.3, ブラウザは主にFirefox10での話。すべてのブラウザで試すほどの元気はなし。

基本的なやりかたの検討
いくつかの方法が考えられるが、なるべく簡単なやつから試していく。
(1)一番最初に検討したのは.htaccessにすべてのファイルを強制ダウンロードするようにヘッダーを仕掛けておき、apacheのIndex機能でファイルリストを表示する方法。即試せるが試して即ボツ。
 (強制ダウンロード用ヘッダーとしては"Header set content-disposition attachment"を使用。)
●Indexを表示するたびに.htaccessへのアクセスは禁止されているとかいったエラーを吐く。自分でアクセスしておいてなんなんだ。
●ごく簡単なファイルの説明などを表示したいがそれができない。
●各ブラウザでいろんなファイルを試しているうち画像ファイル(写真)によっては強制ダウンロードが効かず表示されてしまう場合がある。(これについては後述。)
(2)次に上記の.htaccessの強制ダウンロード用ヘッダーの設定はそのまま使ってインデックスはphpプログラムで行い、単なるリンクでポイントしてあとはapacheにダウンロード処理を任せる方法。
●(1)と同様に一部の画像がダウンロードされずに表示されてしまう現象が起こる。
△ダウンロードフォルダ内にさらにフォルダを作った場合、そのフォルダにも同様にインデックスプログラムを置いていかなくてはいけない。初期のダウンロードさせたいファイルを置くだけでという指針に反するし、めんどい。内部にさらにフォルダは作らないことにすればいいのだが、作りたくなるかもしれない。
(3)インデックスプログラムでインデックス(ファイルリスト)とダウンロード処理を両方やってしまう。
○試した範囲ではどのファイルも問題なく強制ダウンロードできる。
○使い方はファイルやフォルダ、あるいはそのリンクをおくだけ。とても楽。
○今後も含めていろんな要求には最も柔軟にこたえられる。
 たとえばこれを使って普通なら表示されてしまうファイルをダウンロードさせるリンクなども簡単に設置可能。
...というわけで、今回は(3)の方法を採用したのでした。

.htaccessによる方法がうまくいかなかったわけ
 最初最も簡単な方法としてダウンロード用フォルダの.htaccessに、全てのファイルでダウンロード指定のヘッダーを出力するように指定する方法を試した。インデックスはApacheのIndex機能を使えばよい。

 強制ダウンロードの指定は、
Header set Content-Disposition attachment
これだけでいいはず。
ところが、一部の画像ファイル(写真)がそのまま表示されてしまいダウンロードにならない。当初その原因がまったくわからなかった。

こういったのをなんとかダウンロードさせようと、
Header set content-type application/octet-stream
という再生アプリを決定できないファイルタイプを指定したり、あるいは
Header set content-type application/force-downroad
といった存在しないファイルタイプを指定してみたりする手法が流行ってしまったようだ。
再生アプリケーションがなければダウンロードになるということなのだが...。
もっとも当初私もよくわからないままこれらもやってはいた。

 で、この現象をじっくり調べようと別の日に試してみると、再現されない。
そのまま表示されていたファイルもちゃんとダウンロードされる。
ここで思いあたった。ははん、キャッシュだな。
 実は、ダウンロードさせるようなファイルはほとんど更新などしないものだから、キャッシュコントロールなどはいらないと思ってやっていなかった。
さらに上流の.htaccessで画像ファイルなどについては表示スピードを上げるためにわざわざキャッシュするように指定している。いったん無効に。
で、まず次のヘッダーで試す。
Header set Cache-Control no-cache
これで完璧かと思うと決してそうではなかった。実は"Cache-Control no-cache"の意味は、サーバーに再度有効か否かを問い合わせることなくキャッシュを使ってはいけないよという程度の意味だったのだ。
実際にダウンロード指定を外していったん写真を表示させてしまったら再度ダウンロード指定を入れても、ブラウザからは
If-Modified-Since: Tue, 06 Nov 2007 11:11:06 GMT
といった問い合わせが発せられ、これに対しサーバーは
HTTP/1.1 304 Not Modified
と答えている。つまりファイルそのものが更新されたか否かという話だけで、ヘッダーがダウンロード指定に変更されたことなど全く無視されている。

 それではとさらに強力なやつで試してみる。
Header set Cache-Control no-store
これだと一切キャッシュすることをを許可しない。実際いったん写真を表示しておいてダウンロード指定を入れ、再度アクセスするとちゃんとダウンロードされる。
最初これを使うとダウンロードの中断再開はできないかもしれないと思っていたがそれもちゃんとできる。

 ということで結局.htaccessによる強制ダウンロード用ヘッダーとしていまのところ最適と思われるのは、
Header set Content-Disposition attachment
Header set Cache-Control no-store
これだとファイルタイプも正しいものが送出され、仮にPC側でそのまま開くにしても最適なアプリケーションが選ばれる。

さらに私の場合は上流でキャッシュ指示した打ち消しも必要。
<FilesMatch "\.(?i:css|jpg|jpeg|jpe|gif|png|ico)$">
Header set Cache-Control no-store
</FilesMatch>

とにかくダウンロードを確実に実行させるには絶対にキャッシュさせてはならない。

 これでいちおう.htaccessによって強制ダウンロードさせることはできるようにはなったが、apacheのIndex機能を使ってのファイルのリスト表示に多少不満があった。もっとも大きなのは、ファイルの簡単な説明みたいなことを表示したかったということ。
 で、phpスクリプトでファイル名の表示とダウンロードを実行させることにした。
以下、その中でいろいろ検討してわかったこと。ただし書き方はもうphpスクリプトでの表現になっています。

ファイルタイプの指定
 ファイルタイプがわかっているなら、たとえばjpgの写真だと、
header('Content-type: image/jpeg');
どうしてもわからない、あるいは実際に単なるデータの羅列なら、
header('Content-type: application/octet-stream');
 ファイルタイプ(Content-Type)はなるべく正確に出力しましょう。これによりダウンロードダイアログの「表示する」あるいは「アプリケーションで開く」の起動アプリケーションが正しく決定されますし「不明なファイル」と表示されることもなくなります。(実際はoctet-streamとしても多くのブラウザは拡張子やファイルの中身からとにかくファイルタイプを決めようとはしますが。)
 phpからダウンロードさせる場合httpサーバーはこのめんどうを見てくれませんから、拡張子などから自分で決める必要があります。今回はファイルタイプのリストをつくり、それを参照して決めるようにしました。独自のあるいは規格化されていない部分もかなりあるようですが、普通使われるファイルなら特に問題はなさそうです。必要なら追加すればいいし。

表示するかダウンロードするかの指定
 変数$filenameにファイル名が入っているとして、
そのまま表示させるならヘッダーは、
header('content-disposition: inline; filename="'.$filename.'"');
ダウンロードさせるなら、
header('content-disposition: attachment; filename="'.$filename.'"');
ヘッダーを.htaccessに書いてapacheにダウンロードを任せる場合ファイル名の指定はいりませんが(apacheが補填してくれます)、phpから送る場合は必要です。書かないとバッファーIDか何かわけのわからない記号になってしまいます。

検索ロボット制御
 検索ロボットに対しダウンロード用ファイルに対する活動の抑制は入れるべきです。
特にhtmlファイルに対しこれをしておかないと、サンプルとして書いたアドレスやファイルへのアクセスが行われエラーになります。また書いておけば写真等がさらされるのがある程度防げます。
 ただ現在対応しているロボットはgoogle,yahooなど一部のようですので、別途robots.txtなどでもガードしておいた方がいいと思われます。
header('X-Robots-Tag: noindex,nofollow,noarchive');

キャッシュコントロール
 .htaccessによる方法のところで前述したようにこれは必須です。
ただ世にいろいろ言われているように、このブラウザにはこれ効かないとかいうのがあるようで、とりあえず以下のを書いておきました。
header('Expires: 0');
header('Cache-Control: no-store,no-cache,must-revalidate,post-check=0,pre-check=0');

部分ダウンロード
相当大きいファイルをダウンロードさせる場合以外は必要はないけれど、いちおう部分ダウンロードにも対応させてみる。
Accept-Ranges
 これは部分ダウンロード(ダウンロードの中断→再開の際などにまだダウンロードしていない部分だけを指定してダウンロードするなど)を受け入れるか否かの指定。
部分ダウンロードをバイト単位で受け付ける場合、
header('Accept-Ranges: bytes');
この場合当然部分データを要求されたらそのデータを部分ダウンロードの規格に沿って送出するようにプログラムを書いておかなくてはいけない。bytes以外の単位は規格化されておらず、単位は実質bytesしかない。
一方部分ダウンロードを受け入れないなら、
header('Accept-Ranges: none');
 いろいろ調べているうち部分ダウンロードに対応したコードになっていないにもかかわらず'Accept-Ranges: bytes'を書いているサイトをいくつか見つけたが、おおかたどこからか拾ってきてそのまま書いたものだろう。でもデータが変更されていたりして部分ダウンロードが意味をなさないとサーバーが判断した場合などにはすべてのデータを送り直すというのは正しい対応のようだから、実際はダウンロードが最初からやり直しになるだけでダウンロード失敗といった事態にはならないとは思うが。
 .htaccessでhttpサーバーにダウンロードを任せる場合には部分ダウンロード用のモジュールが入っていてそれが有効になっているかどうかで自動的に送出されるだろうからいらないと思う(たぶん)。
HTTP_RANGE
 部分ダウンロードが要求された場合、$_SERVER['HTTP_RANGE']に以下のようなダウンロード範囲の指定値が入ってくる。","で区切って複数指定もありうる。
bytes=1000-2000
bytes=1000-
bytes=-2000
1番目と2番めは直感的に問題はない。1000バイト目から2000バイト目までと、1000バイト目から最後まで。
問題は-2000の場合。直感的には最初から2000バイト目まで(0-2000と同じ)と思うだろうが、そうではない。
これは最後の2000バイトの要求。データが5000バイトあったら3000-4999と同じ。
しかしこれを0-2000と解釈してプログラムを書いて公開しているサイトが実際にあって最初あやうくそれを鵜呑みにしてしまうところだった。
 こんなリクエストはめったにないとは思うが、もしあったらバイト数はちゃんと一致しているのでエラーにもならず全く変なデータを正しいものとして渡してしまうことになる。画像みたいなものならともかく、プログラムだったら何が起こるかわからない。あなおそろしや。

といったところで、スクリプトを書いてdownloadフォルダに設置したのでした。
スクリプトもここに置いてあります。

 なお今回のスクリプトではフォルダごとダウンロードするのには対応していません。
都度アーカイブしてダウンロードさせるのは余計な負荷がかかりますし、先にアーカイブファイルを作って置けばいいだけなので。必要を感じたらやることにしましょう。