ファイルをダウンロードさせる方法(Servlet, HTML, Apache, Tomcat)

Tomcat, Apacheを連携してWebシステムで、ファイルをダウンロードさせるときの設定をまとめました。
【確認した環境】

# 確認したときの環境
* Firefox50
* Windows7
* Java8
    * Servlet v3
* Apache 2.2
* Tomcat7, Tomcat8


# Servletでダウンロード

## コンテンツタイプを`application/octet-stream`にする

```java
response.setContentType("application/octet-stream");
response.getWriter().write("Hello World");
```

https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types

>これは、バイナリー形式ファイル用の既定の値です。実際は未知のバイナリー形式ファイルを表しており、通常ブラウザーは自動的に実行したり、実行すべきであるかを確認したりしません。これらは Content-Disposition ヘッダーの値が attachment であるかのように扱い、ファイルを '名前を付けて保存' することを提案します。

## Content-Dispositionヘッダーの値を`attachment`にする
前述の通り、コンテンツタイプに`application/octet-stream`を指定すればダウンロードできます。
しかし、ダウンロード対象のファイルの種類が分かっている場合、`application/octet-stream`を指定するのは、何だか違和感を感じます。

下記の通り、Content-Dispositionヘッダーに`attachment`を指定すれば、コンテンツタイプが"application/octet-stream"でなくても、ダウンロードできます。

```java
response.setContentType("text/html");
response.setHeader("Content-Disposition","attachment");
response.getWriter().write("Hello World");
```

### ダウンロード時のデフォルトのファイル名
`filename`に指定した値が、ダウンロード時のデフォルトのファイル名になります。

```java
response.setHeader("Content-Disposition","attachment;filename=\"sample.html\"");
// attachment;filename="sample.html"
```

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
>When used in combination with Content-Disposition: attachment, it is used as the default filename for an eventual 'Save As" dialog presented to the user.

ファイル名に全角文字が含まれているときは、`filename*`を使います。

```java
String encodedFilename = URLEncoder.encode("サンプル.html", "UTF-8");
response.setHeader("Content-Disposition","attachment;filename*=\"UTF-8''" + encodedFilename + "\"");
```


https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
>The parameters "filename" and "filename*" differ only in that "filename*" uses the encoding defined in RFC 5987. When both "filename" and "filename*" are present in a single header field value, "filename*" is preferred over "filename" when both are present and understood.


半角空白文字はプラス記号「+」に変換されることに、注意してください。
以下のコードは、ファイル名が`a+b.html`になります。

```java
String encodedFilename = URLEncoder.encode("a b.html", "UTF-8");
response.setHeader("Content-Disposition","attachment;filename*=\"UTF-8''" + encodedFilename + "\"");
```

これは、JavaのURLEncoderの仕様です。
https://docs.oracle.com/javase/jp/8/docs/api/java/net/URLEncoder.html
>空白文字「 」はプラス記号「+」に変換されます。

「+」をさらに`%20`で置換すれば、ファイル名に空白文字が表示されます。
ちなみにURLEncodeは言語によって方言があるので、深入りしないことにします。。。
https://www.glamenv-septzen.net/view/1170


ファイル名に使えない文字はアンダースコアに変換されます。
以下のコードは、ファイル名が`_________.html`になります。

```java
// ¥　/　:　*　?　"　<　>　| を含むファイル名
String encodedFilename = URLEncoder.encode("\\/:*\"<>|.html", "UTF-8");
response.setHeader("Content-Disposition","attachment;filename*=\"UTF-8''" + encodedFilename + "\"");
```


### `filename`と`filename*`の併記

以下のように `filename`と`filename*`を併記すると、`filename*`が有効な場合は`filename`が無視されます。
`filename*`に対応していないブラウザを考慮するならば（2017年にそんなブラウザはある？）、併記するのがよさそうです。

```java
String encodedFilename = URLEncoder.encode("サンプル.html", "UTF-8");
response.setHeader("Content-Disposition","attachment;" + 
  "filename=\"sample.html\"" + 
  "filename*=\"UTF-8''" + encodedFilename + "\"");
```

>Therefore, when both "filename" and "filename*" are present in a single header field
   value, recipients SHOULD pick "filename*" and ignore "filename".

※ https://tools.ietf.org/html/rfc6266#section-4.5 引用
※ [ダウンロードファイル名、文字化けとの格闘](http://negiblog.hatenablog.com/entry/2015/06/19/%25e3%2583%2580%25e3%2582%25a6%25e3%2583%25b3%25e3%2583%25ad%25e3%2583%25bc%25e3%2583%2589%25e3%2583%2595%25e3%2582%25a1%25e3%2582%25a4%25e3%2583%25ab%25e5%2590%258d%25e3%2580%2581%25e6%2596%2587%25e5) 参考

### `filaname*`の二重引用符

RFC6266の例では、`filename`には二重引用符が付いていますが、`filename*`には付いていません。

```txt
Content-Disposition: attachment;
                     filename="EURO rates";
                     filename*=utf-8''%e2%82%ac%20rates
```                          

※ https://tools.ietf.org/html/rfc6266#section-5 引用

`filename*`に二重引用符を付けた場合（`filename*="utf-8''サンプル.html"`）、各ブラウザのダウンロード結果は以下の通りになりました。
 
* Firefox57: OK
* Chrome 63：NG
* Microsoft Edge 41.16299.15.0：NG
* Internet Explorer 11.125.16299.0：NG

Firefox以外では、`filename*`を無視され、ファイル名が"download"でした。

※ https://qiita.com/khsk/items/d541b8dc40bd2c6128d2 参考

# Apache配下のファイルをダウンロード

mod_headersを有効にして、Content-dispositionヘッダーを指定します。
以下のコードは、Apache配下の全てのxmlファイルがダウンロード対象になります。

```config
LoadModule headers_module modules/mod_headers.so

<FilesMatch "\.(xml)$">
    Header set Content-Disposition attachment
</FilesMatch>
```


【参考サイト】
http://qiita.com/kompiro/items/ac60721bc43625a057dc


`http://sample.co.jp/sample.xml`にアクセスすれば、"sample.xml"というファイル名でダウンロードされます。
ファイル名がURLに含まれていることに注意してください。ファイル名にURLとして特別な意味を持つ文字が含まれていると、正しくダウンロードされません。以下の文字を含むファイル名は、正しくダウンロードされませんでした。（Firefoxで確認。ファイル名に使えない文字が、これで全てかどうかは不明）

```txt
 # %
 ```

# HTMLのa要素でダウンロード

a要素にdownload属性を指定すると、download属性に指定した値がファイル名となってダウンロードされます。全角文字や半角空白、URLとして特別な意味を持つ`#`なども使えます。
ただしIEは未対応です…

```html
<a href="sample.png" download="テストa b#%.png">Download</a>
```

https://developer.mozilla.org/ja/docs/Web/HTML/Element/a
>この属性は、URL に移動するのではなくダウンロードするようブラウザーへ示しますので、ユーザーはローカルファイルとして保存することを促されます。属性に値を指定した場合、保存プロンプトのデフォルトのファイル名として解釈します (ユーザーは必要に応じてファイル名を変更できます)。使用可能な値に制限はありませんが、/ および \ はアンダースコアに変換します。多くのファイルシステムにはファイル名に使用できる文字の制限があり、ブラウザーがファイル名を調整するかもしれません。

```html
<a href="sample.png" download="テストa b#%.png">Download</a>
```

download属性に ` ¥　/　:　*　?　"　<　>　| ` を指定すると、全てアンダースコアに変換されました(Firefoxで確認)。
以下のコードはファイル名が`_________.html`になります。

```html
<a href="sample.png" download="&#x5c;/:*?&quot;&lt;&gt;|.png">Download</a>
```

# Tomcat配下のファイルをダウンロードする際の注意
Tomcat7以前(?)は、`URIEncoding`のデフォルトは`ISO-8859-1`です。この文字コードはラテンアルファベットのキャラクタセットで、日本語を表現できません。

https://tomcat.apache.org/tomcat-7.0-doc/config/http.html#Common_Attributes
>This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, ISO-8859-1 will be used. 

そのため、日本語を含むファイルをダウンロードしたいときは、文字コードをUTF-8に設定する必要があります。

```xml:server.xml
<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8" /><!-- ← 追記 -->

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
URIEncoding="UTF-8"  /><!-- ← 追記 -->
```

Tomcat8では、デフォルトがUTF-8なので特に指定は必要ありません。

https://tomcat.apache.org/tomcat-8.0-doc/config/http.html#Common_Attributes
>This specifies the character encoding used to decode the URI bytes, after %xx decoding the URL. If not specified, UTF-8 will be used unless the org.apache.catalina.STRICT_SERVLET_COMPLIANCE system property is set to true in which case ISO-8859-1 will be used.


【参考サイト】
http://qiita.com/kazuki43zoo/items/a365d194f5c4df28912f