Web アプリケーションのファイル(画像・動画・テキスト・CSV)ダウンロードは今更感がありますが、比較的新しい言語の ASP.NET Core で紹介したいと思います。せっかくなので View は jQuery ではなく、TypeScript で実装してみます。
と、その前に。ダウンロードに限ったことではないですが、Web において重要な MIME(マイム)について軽く触れます。
MIMEとは
MIME(マイム)は文書、ファイル、またはバイト列の性質や形式を示す規格です。RFC 6838で定義されています。
では、どんな定義かいくつか見てみます。
- text/css
- text/javascript
- application/octet-stream
- text/plain
“text/css” や “text/javascript” は Web 開発の経験ある方にはお馴染みです。形式としては、タイプ/サブタイプ、タイプ/サブタイプ;引数=値になります。
MIMEは何のためにあるの?
重要:ブラウザーは URL を処理する方法を決定するために、ファイル拡張子ではなく MIME タイプを使用しますので、ウェブサーバーは正しい MIME タイプをレスポンスの Content-Type ヘッダーで送信することが重要です。これが正しく構成されていないと、ブラウザーはファイルの中身を誤って解釈し、サイトが正しく動作しなかったり、ダウンロードファイルが誤って扱われたりすることがあります。
簡単に言うと、Web サーバがブラウザに、このファイルは「画像だよ」「動画だよ」「CSVだよ」って教えます。では、ASP.NET Core MVC でささっと実装して Chrome で確認してみましょう。
image/jpg 表示
ASP.NET Core MVC プロジェクトに Controller と View を作成して、まずは素直に画像を表示します。
コード
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
public class ImageDownloadController : Controller { private IHostingEnvironment _environment; public ImageDownloadController(IHostingEnvironment environment) { _environment = environment; } [HttpGet] public IActionResult Index() { return View(); } // データベース、ファイルシステムなどから取得するところを今回は静的ファイルとして固定で配置 private string _imageFile => Path.Combine(_environment.ContentRootPath, "DownloadFiles\\sample1.png"); [HttpGet] public ActionResult GetImage() { return File(GetFileContens(), "image/png"); } private byte[] GetFileContens() { byte[] fileContens = null; using (var stream = new FileStream(_imageFile, FileMode.Open, FileAccess.Read)) { fileContens = new byte[stream.Length]; stream.Read(fileContens, 0, fileContens.Length); } return fileContens; } } |
View
1 2 3 4 5 6 7 8 9 10 11 12 |
@using ImageDownload.Controllers @{ ViewData["Title"] = "Index"; } <h1>ImageDownload Index.</h1> <p>画像を表示</p> <div class="mb-3"> <img src="@Url.Action("GetImage")" /> </div> |
実装ができたので、View を表示して ImageDownloadController.GetImage の Responseヘッダーを見てみます。
Responseヘッダー
Responseヘッダーは Chrome のデベロッパーツールで確認します。

Response Headers を見ると content-type: image/png となっています。ImageDownloadController.GetImage で指定した contentType の MIMEタイプ と同じですね。
image/jpg ダウンロード
続いて、先ほど作成した Controller に画像をダウンロードするアクションメソッド、View にダウンロードボタンを実装します。
コード
Controller
1 2 3 4 5 |
[HttpGet] public ActionResult Download() { return File(GetFileContens(), "application/octet-stream", "hogehoge.png"); } |
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
@using ImageDownload.Controllers @{ ViewData["Title"] = "Index"; } <h1>ImageDownload Index.</h1> <p>画像を表示</p> <div class="mb-3"> <img src="@Url.Action("GetImage")" /> </div> <p>画像をダウンロード</p> <div class="mb-3"> <input id="btn_download" class="btn btn-info" type="button" value="ダウンロード" /> </div> @section Scripts { <script> $(document).ready(function () { bind(); }); var bind = () => { $('#btn_download').on('click', function () { var a = document.createElement('a'); // Chrome は download 属性なしだと warning になります a.download = 'hogehoge.png'; a.href = '@Url.Action(nameof(ImageDownloadController.Download))'; // Chrome は DOM に append しなくても動作します document.body.appendChild(a); a.click(); document.body.removeChild(a); }); }; </script> } |
※Firefox は、click イベントを発火する前に body などに追加しないと正常に動作しません。
Responseヘッダー
Chrome だとネットワークにリクエストが表示されなかったので Firefox のデベロッパーツールで確認します。

Response Headers に content-type: application/octet-stream、content-disposition という項目があります。
application/octet-stream
これは、バイナリファイルでは既定です。これは未知のバイナリ形式のファイルを表すものであり、ブラウザーはふつう実行したり、実行するべきか確認したりしません。これらは Content-Disposition ヘッダーの値に attachment が設定されたかのように扱い、「名前を付けて保存」ダイアログを提案します。
つまりブラウザは表示しようとせず、ダウンロードする MIME タイプです。
Content-Disposition
通常の HTTP レスポンスにおける Content-Disposition レスポンスヘッダーは、コンテンツがブラウザでインラインで表示されることを求められているか、つまり、Webページとして表示するか、Webページの一部として表示するか、ダウンロードしてローカルに保存する添付ファイルとするかを示します。
attachment はダウンロードすべきであることを示し、ブラウザは filename パラメータの値を初期値として「名前を付けて保存」ダイアログを表示します。
Fetch, XMLHttpRequestでダウンロード
$.ajax() はバイナリをテキスト扱いしてしまうため Fetch、 XMLHttpRequest で実装します。
因みに $.ajax() で指定できる dataType は、”text”, “html”, “xml”, “script”, “json”, “jsonp” となります。
コード
TypeScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
class ImageGetter { constructor() { } public async display(url: string, img: HTMLImageElement): Promise<void> { try { const blob = await this.getImage(url); img.src = URL.createObjectURL(blob); } catch (error) { console.log(`download error : ${error.message}`); } } public async download(url: string, executeFetch: boolean = true): Promise<void> { if (executeFetch) { // Fetch await this.getImageFetch(url); } else { // XMLHttpRequest try { const blob = await this.getImageXhr(url); this.downloadImage(blob); } catch (error) { console.log(`download error : ${error.message}`); } } } private async getImageXhr(url: string): Promise<Blob> { return await this.getImage(url); } private getImage(url: string): Promise<Blob> { return new Promise(function (resolve: (value?: Blob) => void, reject: (reason?: Error) => void) { var req = new XMLHttpRequest(); req.open('GET', url); req.responseType = 'blob'; req.onload = function () { if (req.status === 200) { resolve(req.response); } else { reject(new Error(req.statusText)); } }; req.onerror = function () { reject(new Error('network error')); }; req.send(); }); } private async getImageFetch(url: string): Promise<void> { try { let response = await fetch(url); if (response.ok) { this.downloadImage(await response.blob()); } else { throw new Error('network error'); } } catch (e) { console.log(`${e.name}: ${e.message}`); } } private downloadImage(blob: Blob): void { var a = document.createElement('a'); a.download = 'hogehoge.png'; a.href = URL.createObjectURL(blob); document.body.appendChild(a); a.click(); document.body.removeChild(a); } } |
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
@using ImageDownload.Controllers @{ ViewData["Title"] = "Index"; } <h1>ImageDownload Index.</h1> <p>画像を表示</p> <div class="mb-3"> <img src="@Url.Action("GetImage")" /> </div> <p>画像をダウンロード</p> <div class="mb-3"> <input id="btn_download" class="btn btn-info" type="button" value="ダウンロード" /> </div> <p>画像を XMLHttpRequest(typescript)で表示</p> <div class="mb-3"> <img id="img_xhr" /> </div> <div class="mb-3"> <input id="btn_xhr_display" class="btn btn-info" type="button" value="表示" /> </div> <p>画像を Fetch, XMLHttpRequest(typescript)でダウンロード</p> <div class="mb-3"> <input id="btn_javascript_download" class="btn btn-info" type="button" value="ダウンロード" /> </div> @section Scripts { <script> var imageGetter = new ImageGetter(); $(document).ready(function () { bind(); }); var bind = () => { const url = '@Url.Action(nameof(ImageDownloadController.GetImage))'; $('#btn_download').on('click', function () { var a = document.createElement('a'); a.download = 'hogehoge.png'; a.href = '@Url.Action(nameof(ImageDownloadController.Download))'; document.body.appendChild(a); a.click(); document.body.removeChild(a); }); $('#btn_xhr_display').on('click', function () { imageGetter.display(url, document.getElementById('img_xhr')); }); $('#btn_javascript_download').on('click', function () { imageGetter.download(url); }); }; </script> } |
まとめ
ASP.NET Core と TypeScript を使ってファイルのダウンロードを簡単に実装しました。通常の ajax を使うと text でかえってくるところとか忘れているとあれ?!となるので覚えておくといいかもしれません。他にも覚えておいた方が良いことは、download 属性は同一オリジンの URL に限り動作するということです。これを忘れていると指定したファイル名とダウンロードしたファイル名が違う?!と少しはまってしまうかもしれません。