WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ダウンロードの際の拡張子 と MIME Type の指定

by WebSurfer 2010年9月2日 22:42

MSDN フォーラムで、IE で xlsm をダウンロードすると zip 形式になってしまい、直接 Excel で開けないという話があったので、ちょっと調べてみました。

結局、単にそれはサーバー側の Web アプリの問題だったのですが、調査の結果、自分が知らなかったことがいろいろあったので、忘れないように書いておきます。

ブラウザは、ヘッダーの Content-Disposition: attachment; filename= で指定する拡張子と、Content-Type: で指定する MIME Type のどちらでファイルの種類を判断するでしょうか?

検証した結果は以下のとおりでした。簡単に言うと、IE8 は拡張子で判断、Opera は MIME Type で判断、Forefox は拡張子と MINE Type の指定が違う場合結果は未定義のようです。

ブラウザ Ext: xlsm
MT: xlsm
Ext: xlsm
MT: zip
Ext: zip
MT: zip
Ext: zip
MT: xlsm
IE8 Excel Excel Zip Zip
Firefox 3.6.8 Excel Zip Zip Zip
Opera 10.61 Excel Zip Zip Excel

上の表で、Ext は Content-Disposition に設定した拡張子、MT は Content-Type に設定した MIME Type です。

検証に使ったファイルは、有効な xlsm 形式のファイルで、拡張子は正しく xlsm としており、検証の間、中身も名前も一切変えていません。

クライアントの OS は Vista SP2 です。拡張子とアプリケーションの関連付けは、xlms は Excel、zip は WinZip としています。Web サーバーは Windows Server 2008 の IIS7, ASP.NET 3.5 SP1 です。

予想外の動きをしたのは Opera で、拡張子と MIME Type が異なる場合、拡張子を MIME Type に合わせて書き換えてしまいます。下の画像は、拡張子を xlsm、MIME Type を zip とした場合の例です。元のファイル名 001.xlsm が 001.zip に書き換えられています。

Opera のファイルダウンロード時のダイアログ

直リンクの場合(a 要素の href 属性にサーバーに置いた xlsm ファイルを指定した場合)、ヘッダーは以下のようになります。ブラウザは正しく判断出来るようです。

Content-Type: application/vnd.ms-excel.sheet.macroEnabled.12
Last-Modified: Thu, 02 Sep 2010 13:07:58 GMT
Accept-Ranges: bytes
ETag: "3e8647dd9f4acb1:0"
X-Powered-By: ASP.NET
Date: Thu, 02 Sep 2010 13:12:12 GMT
Content-Length: 12477

ちなみに、HTTP ハンドラーを使った場合のヘッダーは以下のようになります。

Cache-Control: private
Content-Type: application/vnd.ms-excel.sheet.macroEnabled.12
Content-Disposition: attachment; filename=001.xlsm
X-AspNet-Version: 2.0.50727
X-Powered-By: ASP.NET
Date: Thu, 02 Sep 2010 13:21:00 GMT
Content-Length: 12477

検証に使った HTTP ハンドラのコードを参考までにアップしておきます。

<%@ WebHandler Language="C#" Class="_045_Handler" %>

using System;
using System.Web;
using System.IO;

public class _045_Handler : IHttpHandler 
{
  public void ProcessRequest(HttpContext context)
  {
    string path = 
      context.Server.MapPath("~/Test/data/001.xlsm");

    if (!File.Exists(path))
    {
      return;
    }
        
    string type = context.Request.QueryString["type"];

    switch (type)
    {
      case "xlsm_xlsm":
        SetHeader(context, "001.xlsm", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;
      case "xlsm_zip":
        SetHeader(context, "001.xlsm", 
          "application/x-zip-compressed");
        break;
      case "zip_zip":
        SetHeader(context, "001.zip", 
          "application/x-zip-compressed");
        break;
      case "zip_xlsm":
        SetHeader(context, "001.zip", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;
      default:
        SetHeader(context, "001.xlsm", 
          "application/vnd.ms-excel.sheet.macroEnabled.12");
        break;                
    }
    context.Response.TransmitFile(path);
    context.Response.End();
  }
    
  public bool IsReusable
  {
    get {
      return false;
    }
  }

  protected void SetHeader(HttpContext context, string name, string type)
  {
    context.Response.AppendHeader("Content-Disposition",
      "attachment; filename=" + name);
    context.Response.ContentType = type;
  }
}

他に、今回知った新(?)事実は、Excel 2007 からファイル形式が変わったということです。何をいまさらと言われるかも知れませんが。(汗)

Excel 2007 からはファイルはバイナリではなくなり、xml 形式のテキストを zip で圧縮したものになっているそうです。確かに WinZip で xlsm ファイルを開くことができ、中身は xml 形式のテキストファイルでした。

また、xlsm は「マクロ有効ブック」というそうで、セキュリティ対策として、拡張子からマクロが含まれていることが分かるような名前の規則を作ったそうです。

------------ 2011/5/21 追記 ------------

IE8 は拡張子で判断と書きましたが、やはり IE はそのような仕様になっているようです。以下のページを参考にしてください。

ファイルのダウンロードダイアログで表示されるファイル名の命名規則

即ち、上のページの「詳細」の 1 番目に書いてあるとおり「Content-Disposition: ヘッダが存在する場合は、filename パラメータで設定されたファイル名が利用されます。」ということです。

サポートページには書いてないですが、Content-Disposition: ヘッダでファイル名が指定してあれば、Content-Type: の指定は無視されます。

2 番目の「HTTP レスポンス内に存在する、送られてきた Content-Type が、レジストリの HKEY_CLASS_ROOT\MIME\Database\Content Type 以下に存在するかどうか・・・」というのは、Content-Disposition: ヘッダの filename パラメータにファイル名の指定がない場合に限るようです。

Tags: ,

Upload Download

ダウンロードしたファイルの更新日時

by WebSurfer 2010年8月30日 16:21

クライアントがサーバーからファイルをダウンロードし、自分の HDD などに保存した場合、そのファイルの更新日時は HDD に保存した日時になります。

ファイルのダウンロード

これをサーバー側で設定した日付にすることはできるでしょうか? ActiveX などのプラグインは使わないという条件でです。

結論から言えば、世間一般に使われている解凍ツールと互換性のある zip アーカイブを使えば可能です。

サーバーからは zip アーカイブに更新日時情報を含めて送信し、クライアント側で解凍して保存する時に zip アーカイブに含まれている更新日時に設定してもらうということです。

Web アプリケーションとしては、以下ような手順になります。

  1. データベースまたはファイルシステムからバイト列データを取り出す。
  2. 既存のライブラリを利用して zip アーカイブをバイト列として作成。更新日時やファイル名はその時設定。
  3. 作成したバイト列を Response.BinaryWrite メソッドで HTTP 出力ストリームに書き込み

参考に、フリーのライブラリ SharpZipLib を使った例をアップしておきます。実行するには Bin フォルダに ICSharpCode.SharpZipLib.dll を入れておく必要があります。下記のサイトから入手できます。

The Zip, GZip, BZip2 and Tar Implementation For .NET

<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="ICSharpCode.SharpZipLib.Zip" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  protected void Page_Load(object sender, EventArgs e)
  {
    // 例として、SQL Server DB からデータを取得する
    // ことで考えています。
    ConnectionStringSettings cs =
      ConfigurationManager.ConnectionStrings["MyDB"];
    string connString = cs.ConnectionString;
    SqlConnection connection = new SqlConnection(connString);
    string query = 
      "SELECT filename, data FROM files WHERE id=@id";
    SqlCommand command = new SqlCommand(query, connection);
    command.Parameters.AddWithValue("@id", 
      Convert.ToInt32(Request.QueryString["id"]));
    string filename = null;
    byte[] data = null;
       
    try
    {
      connection.Open();
      SqlDataReader reader = command.ExecuteReader();

      if (reader.Read())
      {
        filename = (string)reader[0];
        data = (byte[])reader[1];
      }
    }
    finally
    {
      connection.Close();
    }

    if (filename == null || data == null)
    {
      // DB から取得できなかった場合の処置。
      // ここでは単に return
      return;
    }

    Response.AppendHeader("Content-Disposition",
      "attachment; filename=" + 
      HttpUtility.UrlEncode(filename) + ".zip");
    Response.ContentType = "application/x-zip-compressed";

    // 何故かバイト列のサイズを指定した方が圧縮率
    // が高くなる。圧縮前よりは小さくなるはずなの
    // で、とりあえず data.Length とする。
    byte[] zippedData = new byte[data.Length];
    int zippedDataLength = 0;
    using (MemoryStream memoryStream = 
      new MemoryStream(zippedData))
    {
      using (ZipOutputStream zipOutStream = 
        new ZipOutputStream(memoryStream))
      {
        // 圧縮度の設定。9 が最高
        zipOutStream.SetLevel(9);

        ZipEntry entry = new ZipEntry(filename);

        // ここで更新日時を設定
        entry.DateTime = new DateTime(2000, 1, 1);

        zipOutStream.PutNextEntry(entry);
        zipOutStream.Write(data, 0, data.Length);               
        zipOutStream.Finish();

        // 圧縮後のサイズを取得。Finish() の前に置く
        // のは NG。zipOutStream.Length は NG。
        zippedDataLength = (int)zipOutStream.Position;

        zipOutStream.Close();
      }
    }       
    Array.Resize(ref zippedData, zippedDataLength);
    Response.BinaryWrite(zippedData);
    Response.End();
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
  </div>
  </form>
</body>
</html>

そもそも、ファイル名、更新日時その他のメタデータはファイルシステムが保持しているもので、ファイル本体には含まれていません。

ファイル名のみは、ブラウザが自動的に attachment; filename= に指定された名前で保存してくれますが(日本語の文字化けの問題はありますが)、その他の情報はダメなようです。

Content-Disposition ヘッダーフィールドにファイル名、作成日時、更新日時等の情報を追加してクライアントに送ることは可能です。でもそれは送る手段が用意されているというだけで、ブラウザが自動的にそれに書き換えてくれることはありません。

Tags: , ,

Upload Download

ダウンロードは別ウィンドウで

by WebSurfer 2010年8月6日 20:52

ファイルをダウンロードする際、ダウンロード後にも何かの処置を継続し、その結果を表示したい場合はどうすればいいのでしょう?

例えば、Button をクリックするとポストバックがかかり、その Click イベントのハンドラでファイルをダウンロードし、その後そのページを継続して表示するが、ダウンロード後は表示したくない部分がある場合を考えて見ます。

ファイルダウンロード後ページの一部を隠す

ダウンロード後は表示したくない部分を Panel に入れて、Click イベントで Panel.Visible プロパティを false に設定するという手段を考えると思います。

しかしながら、ファイルのダウンロードとその処置を 1 ページで行うとうまくいきません。

何故なら、ボタンがクリックされてポストバックがかかり、サーバーから送られてくるのは HTTP 応答ヘッダとダウンロードされるファイルのバイト列だけだからです。

ボタンクリック後もポスト前の画面が表示されているのは、ブラウザにポスト前の画面が保持されているからで(ここのところ確証はないのですが)、ポストバック後サーバーから送られてきたものが表示されているわけではありません。前の画面だから、Panel の中身は表示されたままです。

Response.End(), Response.Flush() などをしなかったら、Download も処置の継続もうまくのではと思って、いろいろ試してみましたが、1 ページで処置するのは無理でした。

というわけで、ファイルをダウンロードする別のページを作って、それを呼び出すのがよさそうです。例えば、以下のような感じです。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">
  protected void Button1_Click(object sender, EventArgs e)
  {
     Panel1.Visible = false;
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>ファイルダウンロード</title>
</head>
<body>
  <form id="form1" runat="server">
  <div>
    <h1>ファイルダウンロード後に以下の部分を隠す</h1>
    <asp:Panel ID="Panel1" runat="server">
      <div style="background-color: Silver">
        <h2>隠す部分</h2>
        <asp:Button ID="Button1" 
          runat="server" 
          Text="Download" 
          onclick="Button1_Click" 
          OnClientClick="window.open('Download.aspx', null);" />
     </div>
   </asp:Panel>    
  </div>
  </form>
</body>
</html>

ダウンロードを行う別ページは、何をどのようにダウンロードするかによって千差万別ですが、DB からデータを取得して CSV ファイルにしてダウンロードする場合の例を、ついでにアップしておきます。

この例では、デリミタに使っている改行やコンマがフィールドに含まれているとうまくいませんので注意してください。

<%@ Page Language="C#" ContentType="application/octet-stream" 
ResponseEncoding="Shift_JIS" %>
<%@ Import Namespace="System.Data.SqlClient" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

  protected void Page_Load(object sender, EventArgs e)
  {        
    string connString = 
      ConfigurationManager.ConnectionStrings["Pubs"].ConnectionString;
    string query = "SELECT * FROM employee";
    SqlConnection sqlConn = new SqlConnection(connString);
    SqlCommand sqlCmd = new SqlCommand(query, sqlConn);
    string csvString = String.Empty;
    try
    {
      sqlConn.Open();
      SqlDataReader reader = sqlCmd.ExecuteReader();
      while (reader.Read())
      {
        for (int i = 0; i < reader.FieldCount; i++)
        {
          if (i == reader.FieldCount - 1)
          {
            csvString += reader[i].ToString() + "\r\n";
          }
          else
          {
            csvString += reader[i].ToString() + ",";
          }
        }
      }
    }
    finally
    {
      sqlConn.Close();
    }

    Encoding encode = Encoding.GetEncoding("shift_jis");
    Response.AppendHeader("Content-Disposition", "attachment; filename=test.csv");
    Response.Write("Employee ID,First Name,Middle Initial,Last Name,Job ID,Job Level,Pub ID,Hire Date\r\n");
    Response.BinaryWrite(encode.GetBytes(csvString));
    Response.End();
  }
</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title>無題のページ</title>
</head>
<body>
  <form id="form1" runat="server">
  <div>    
  </div>
  </form>
</body>
</html>

なお、IE8 の場合、空白のウィンドウが一旦開いてしまい、「ファイルのダウンロード」ダイアログのボタンをクリックするまで閉じないという気に入らない点があります。

今のところ回避策が見つかっていません。回避策をご存知の方はアドバイスいただけるとうれしいです。

Tags: ,

Upload Download

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar