vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 1

はじめに

(いつものごとく) とある事情で PDF をダウンロード・アップロードする機能が欲しかったため、試してみたら苦労した、という話です。

以前 ASP.NET Core でローカルの特定のディレクトリにあるファイルをアクセス可能にしたことがありましたが、今回も同様に、特定のディレクトリに PDF ファイルを置き、それをダウンロードする機能と、そのディレクトリにファイルをアップロードする機能を作ってみます。

exports が見つからない

のっけから話の腰を折り曲げるわけですが。

今回の検証用に ASP.NET Core プロジェクトを作り、 npm install で TypeScript や webpack などを追加したところ、以前 webpack でグローバル関数を HTML からみられるようにするために使っていた 「 exports.greeting() = async function greeting(){ ~ } 」の exports が見つからないというエラーに遭遇しました。

結局のところ、 @types/node をインストールすることで解決しました。

npm install --save @types/node

型が無いからエラーに、というのはわかるのですが、これまではなぜエラーになっていなかったのか。。。

準備

まずは準備から。

PDF ファイルはプロジェクトと同じ場所に置いておきます。

で、 Static file として公開します。

Startup.cs

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;

namespace DownloadUploadSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            
            app.UseStaticFiles(new StaticFileOptions {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample", "files")),
                RequestPath = "/files"
            });
            
            app.UseMvc();
        }
    }
}

HomeController.cs

using Microsoft.AspNetCore.Mvc;

public class HomeController: Controller{
    [Route("/")]
    public IActionResult Index(){
        return View("./Views/Index.cshtml");
    }
}

ダウンロードもアップロードも Index.cshtml から実行することにします(個別で作るの面倒なので)。

PDF をダウンロードする

まずはファイルに対する Link を設定して、ダウンロードできるか試してみます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' target="_brank">PDF< /a>

結果としては、ダウンロードされるのではなくブラウザの PDF Viewer で表示されました。

一応 target="_brank" も外してみましたが、単に開いているタブで表示されるだけで、ダウンロードされることはありませんでした。

download 属性を追加する

モダンなブラウザであれば、下記のように download 属性をつけることで、強制的にダウンロードさせることができます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' download>PDF< /a>

めでたしめでたしといいたいところですが、この属性、 IE だと( ver.11 でも)無視されます/(^o^)\

またか。。。

※なぜか家の IE 11 で確認したところ問題なくダウンロードのポップアップが表示されました。。。

なんでや。。。orz

でも話が終わってしまうので、できなかった体で進めます。

msSaveOrOpenBlob

IE 、 Edge で使うことのできる、 msSaveOrOpenBlob を利用してダウンロードする方法も試してみました。

ほぼ上記のコピペではあったものの、ファイルがダウンロードできることが確認できました。

なぜこれを選ばなかったかというと、下記 2 つの理由によります。

  1. IE 、 Edge でしか使う必要がない (しかも Edge は download 属性に対応しているためこの用途では不要)
  2. 「ダウンロードして開く」「保存する」のポップアップがファイルダウンロード後に表示される

2についてはリンククリック時にポップアップを出して、スピナーなどを出して。。。という機能を用意すれば良いのですが、 UI 的にこだわりたい場合はともかく、ただダウンロードできれば良いレベルの場合も、というのはちょっとツラいですね。

Web ブラウザーが表示対応しているばかりにこんなことに。。。(いつもは便利に使わせていただいております (..)_ )

ASP.NET Core の機能でダウンロード

いったん JavaScript で何とかするのをあきらめて、今回は ASP.NET Core 側で何とかしてみることにしました。

HomeController.cs

~省略~
[Route("/files/pdf")]
public IActionResult DownloadPdfFile(){
    return DownloadFile(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files", "sample.pdf");
}
private FileResult DownloadFile(string filePath, string fileName)
{
    IFileProvider provider = new PhysicalFileProvider(filePath);
    IFileInfo fileInfo = provider.GetFileInfo(fileName);
    Stream readStream = fileInfo.CreateReadStream();
    string mimeType = "application/pdf";
    return File(readStream, mimeType, fileName);
}
~省略~

ファイルを Stream に変換し、 FileResult として返すことで PDF として扱われなくなる、ということなのでしょうか。

やっぱりやりたいことに対して大げさすぎる気がしないでもないですが、ともあれ PDF がダウンロードできるようになりました。

FormData を使って PDF をアップロードする

ASP.NET Core のドキュメントを見ると、ファイルのアップロードには フォームデータとしてアップロードする方法と、 Stream でアップロードする方法があるとのこと。

あまりファイル容量が大きい場合は向かないとのことですが、まずはフォームデータを試してみたいと思います。

Index.cshtml

~省略~
< input type='file' id='upload_file_form'>
< button onclick='Page.uploadFile()' accept='pdf'>send< /button>
< script src='./src/js/main.bundle.js'>< /script>

mainPage.ts

exports.uploadFile = async function uploadFile(){
    // HTMLInputElement.files で 参照ボタンを押して選択されたファイルが取得できる.
    const fileField = document.getElementById("upload_file_form") as HTMLInputElement;

    if(fileField === null ||
        fileField.files === null ||
            fileField.files.length <= 0){
        return;
    }
    // フォームに入れて fetch で送る.
    const formData = new FormData();
    formData.append('file', fileField.files[0]);

    await fetch('files/pdf', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .catch(error => console.error('Error:', error))
    .then(response => console.log('Success:', JSON.stringify(response)));
}

HomeController.cs

~省略~
[HttpPost]
[Route("/files/pdf")]
public async Task UploadPdfFile(IFormFile file){
    if(file == null){
        return false;
    }
    // 受け取ったファイルを FileStream に変換し、ファイル名を変更して保存.
    using(var stream = new FileStream(
            @"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files\" +
                DateTime.Now.ToString("yyyyMMddHHmmss") + 
                    file.FileName, FileMode.Create)){
        await file.CopyToAsync(stream);        
    }

    return true;
}
~省略~

なぜか fetch の option として、 headers: { "Content-Type": "application/pdf" } を渡すと IFormFile が null になってしまうなど、しっくり来ていないところも多いですが、とにかくファイルを送信することはできました。

ただ、ファイルの容量が数十 MB など大きくなってくると、 ASP.NET Core 側でエラーが発生してしまいます。

ググったところ、この容量制限を緩和することはできるようですが、 ASP.NET Core のドキュメントでも挙げられている通り Stream を使うのが良さそうです。

ということで、次回は Stream を使ったアップロードの話。。。の予定。