Monitoring downloads

by 2 contributors:

Firefox 3 では、ダウンロード状況の監視がかつてなく簡単になりました。これまでのバージョンの Firefox でも実装は可能でしたが、ひとつのオブザーバを一度に登録することしかできませんでした。Firefox 3 では、ダウンロードの監視にいくつでもリスナーを利用できる新しい API が導入されました。

この記事では、Firefox 3 のダウンロードマネージャを利用してダウンロードを監視する方法を実演します。また、親切なおまけとして、Storage API を利用してデータベースに sqlite コマンドを発行する方法も実演します。その結果は、[ツール] メニューの [Download log] を選択することで開けるウィンドウ上で見ることができます。このウィンドウでは、サンプル用の拡張機能をインストールしてから行われたすべてのダウンロードが一覧表示されます。一覧には、ファイル名、ダウンロード開始時刻と終了時刻、ダウンロード速度、ダウンロード状況が表示されます。ファイルの取得元 URL を示すツールチップも含まれています。

完全なサンプルをダウンロード

セットアップ

拡張機能の読み込みが完了すると、すぐにいくつかの処理が行われます。具体的には、ダウンロードマネージャの nsIDownloadManager インタフェースのインスタンスを取得し、データを保存するためのデータベースを作成するのに、これらの処理が必要となります。

 onLoad: function() {
   // 初期化コード
   this.initialized = true;
   this.strings = document.getElementById("downloadlogger-strings");
   
   this.dlMgr = Components.classes["@mozilla.org/download-manager;1"]
                          .getService(Components.interfaces.nsIDownloadManager);
             
   this.dlMgr.addListener(downloadlogger);
   
   // データベースを開き、そのファイルをプロファイルディレクトリに保存します
   
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   
   // ストレージサービスへのアクセスを取得し、データベースを開きます
   
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   
   var dbConn = this.storageService.openDatabase(this.dbFile);
       
   // テーブルを作成します。既に存在する場合は失敗しますが、気にしません。
   
   dbConn.executeSimpleSQL("CREATE TABLE items (source TEXT, size INTEGER," +
                           " startTime INTEGER, endTime INTEGER," +
                           " speed REAL, status INTEGER)");
   dbConn.close();
 },

これは非常に簡単な例です。ダウンロードマネージャのインスタンスは、後で再利用できるよう downloadlogger オブジェクトのメンバー変数にキャッシュされ、addListener() メソッドが呼び出されてダウンロード状況の監視が開始されます。データベースファイルが開かれ、sqlite の CREATE TABLE コマンドが実行されてテーブルが作成されます。

最後に、データベースが閉じられます。

注意: mozIStorageConnectionclose() メソッドは Firefox 3 Alpha 8 で追加されました。Firefox の以前のバージョンでは、データベースを明示的に閉じる方法がありません。その代わり、ガベージコレクタが接続オブジェクトを破棄したときに閉じられます。

ダウンロード状況変更時の処理

上記のコードが実行されたら、ダウンロードの状況が変わるたびに onDownloadStateChange() メソッドが呼び出されます。これは nsIDownloadProgressListener インタフェースの一部です。

この部分のコードは以下のようになります:

 onDownloadStateChange: function(aState, aDownload) {
   var statement;
   
   switch(aDownload.state) {
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
     
       // 開始されたダウンロードのために新しい列を追加します。各列には取得元 URI、
       // サイズ、開始時刻が含まれます。終了時刻とダウンロード速度は、まだ分からないため、
       // 初めはいずれも 0 に設定します。
       
       // 状況は、ダウンロードマネージャから提供されるものと同じ、状況を示す値になります。
       
       var dbConn = this.storageService.openDatabase(this.dbFile);
       statement = dbConn.createStatement("REPLACE INTO items VALUES " +
                                          "(?1, ?2, ?3, 0, 0.0, 0)");
 
       statement.bindStringParameter(0, aDownload.source.spec);
       statement.bindInt64Parameter(1, aDownload.size);
       statement.bindInt64Parameter(2, aDownload.startTime);
       statement.execute();
       statement.reset();
       dbConn.close();
       break;
       
     // ダウンロードの完了 (失敗もしくは成功) を記録します
 
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FAILED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_CANCELED:
       this.logTransferCompleted(aDownload);
       break;
   }
 },

ここで 4 つの進捗状況に注目してみましょう。aDownload.state フィールドで示されるダウンロード状況が Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING の場合、ファイルのダウンロードが開始されています。aDownload オブジェクトは nsIDownload オブジェクトです。

その場合、新しいファイルのために、データベースを開いて REPLACE INTO sqlite コマンドを作成することで、データベースに新しい列を作成します。最初の 3 列は、ダウンロードオブジェクトから提供された、取得元 URI、ファイルサイズ、開始時刻フィールドの値に設定されます。残りの列の情報は、この時点では分からないため、ゼロに設定されます。

ダウンロードの進捗状況が、ダウンロードが完了、キャンセル、あるいは失敗したことを示した場合、logTransferCompleted ルーチンを呼び出して、その進捗状況の変更を示すようログを更新します。この部分のコードは以下のようになります。

 logTransferCompleted: function(aDownload) {
     var endTime = new Date();                // 現在時刻が終了時刻になります
     
     // REPLACE sqlite コマンドを発行して記録を更新します。同じ取得元 URI と開始時刻の
     // 記録を見つけたら、その記録内の終了時刻、サイズ、速度のエントリーを更新します。
     // 取得元 URI と開始時刻の両方が一致することを確認することで、同じファイルについて
     // 複数ダウンロードがあっても、それぞれに記録を取ることができます。
     
     var dbConn = this.storageService.openDatabase(this.dbFile);
     var statement = dbConn.createStatement("UPDATE items SET size=?1, " + 
         "endTime=?2, speed=?3, status=?4 WHERE source=?5 and startTime=?6");
     statement.bindInt64Parameter(0, aDownload.size);
     statement.bindInt64Parameter(1, endTime.getTime());
     statement.bindDoubleParameter(2, aDownload.speed);
     statement.bindInt32Parameter(3, aDownload.state);
     statement.bindStringParameter(4, aDownload.source.spec);
     statement.bindInt64Parameter(5, aDownload.startTime);
     statement.execute();
     statement.reset();
     dbConn.close();
 },

ここでは単純に、データベースを開いて、UPDATE sqlite コマンドを作成、実行することで、完了したダウンロードと取得元 URI と開始時刻が一致するダウンロード項目を検索し、その情報を更新しています。同じ URI と開始時刻の記録を探すことで、ユーザが同じファイルを何度ダウンロードした場合も、正しく処理を行うことができます。

ダウンロードログの表示

ダウンロードログウィンドウのコードは、downloadlogger_dlwindow と呼ばれるオブジェクトの中で完結しています。これはシンプルな例なので、1 回限りのログウィンドウとなっており、それ以降のログの変更は監視していません。ウィンドウが開かれた時点でのダウンロードの状況を単純に表示するだけです。

つまり、ここでの処理はすべて load イベントハンドラだけで行うことができます。コードは以下の通りです。

 onLoad: function() {    
   // データベースを開きます
   
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   
   // ストレージサービスへのアクセスを取得し、データベースを開きます
   
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   
   var dbConn = this.storageService.openDatabase(this.dbFile);
   
   var loglist = document.getElementById("loglist");
 
   var statement = dbConn.createStatement("SELECT * FROM items");   // テーブル内のすべての項目を取得します
   try {
     while (statement.executeStep()) {
       var row = document.createElement('listitem');
       
       // 列にセルを追加します
       
       var cell = document.createElement('listcell');
       var sourceStr = statement.getString(0);
       row.setAttribute("tooltiptext", sourceStr);
       sourceStr = sourceStr.slice(sourceStr.lastIndexOf("/")+1, sourceStr.length);
       cell.setAttribute("label", sourceStr);   // 取得元
       row.appendChild(cell);
       
       cell = document.createElement('listcell');
       cell.setAttribute("label", (statement.getInt64(1) / 1024).toFixed(1) + "KB");    // サイズ
       cell.setAttribute("style", "text-align:right");
       row.appendChild(cell);
       
       var theDate = new Date(statement.getInt64(2) / 1000);        // 開始時刻
       cell = document.createElement('listcell');
       var dateStr = theDate.toLocaleString();
       cell.setAttribute("label", dateStr);
       row.appendChild(cell);
       
       theDate = new Date(statement.getInt64(3));            // 終了時刻
       cell = document.createElement('listcell');
       dateStr = theDate.toLocaleString();
       cell.setAttribute("label", dateStr);
       row.appendChild(cell);
       
       var speed = statement.getDouble(4) / 1024.0;
       cell = document.createElement('listcell');
       cell.setAttribute("label", speed.toFixed(1) + "KB/sec");
       cell.setAttribute("style", "text-align:right");
       row.appendChild(cell);
       
       var status = statement.getInt32(5);
       var style = "color:black";
       cell = document.createElement('listcell');
 
       var statusStr;
       
       switch(status) {
         case 0:
           statusStr = "Downloading";
           break;
         case 1:
           statusStr = "Complete";
           style = "color:green";
           break;
         case 2:
           statusStr = "Failed";
           style = "color:red";
           break;
         case 3:
           statusStr = "Canceled";
           style = "color:purple";
           break;
         case 4:
           statusStr = "Paused";
           style = "color:blue";
           break;
         case 5:
           statusStr = "Queued";
           style = "color:teal";
           break;
         case 6:
           statusStr = "Blocked";
           style = "color:white background-color:red";
           break;
         case 7:
           statusStr = "Scanning";
           style = "color:silver";
           break;
         default:
           statusStr = "Unknown";
           break;
       }
       cell.setAttribute("label", statusStr);
       cell.setAttribute("style", style);
       row.appendChild(cell);
       
       loglist.appendChild(row);
     }
   } finally {
     statement.reset();
     dbConn = null;
   }
 }

このコードは至ってシンプルです。初めに、ログ情報が含まれる sqlite データベースを開いた後、SELECT SQL 構文を作成し、データベースからすべてのエントリーを取得します。

複数の結果を繰り返し処理するために、mozIStorageStatement オブジェクトの executeStep() メソッドを呼び出す while ループを使っています。このメソッドが呼び出されるたびに、結果から 1 つの列が取得されます。

その後、リスト列オブジェクトが作成され、検索結果の各エントリーが取得されて適切なリストセルに挿入されます。

上記のコードからいくつか興味深い点を取り上げてみましょう。

  • mozIStorageStatement は、getString()getDouble()getInt64() といった、検索結果を取得するためのデータ取得ルーチンをいくつか持っています。これらのメソッドは、取得したい値を持つ列のゼロベースのインデックス番号をパラメータとして取ります。
  • 開始時刻は、その値から JavaScript Date オブジェクトを作成する前に、1000 で割られていることに注意してください。これは、データベースに保存されている時間の精度を、JavaScript で使用される値に変換するためです。
  • 数値の列を右寄せするために、適切なセルの style 属性に text-align:right を設定しています。

読者への課題

この拡張機能を改良するためにできる、一見して分かることがいくつかあります。ダウンロードマネージャやストレージ API の使い方を学んでいるなら、以下のようなことを、練習のために調べてみると良いでしょう。

  • 早速、ダウンロードログウィンドウで、ウィンドウが開かれたときに静的なリストを生成する代わりに、動的に更新するためのコードを追加してみましょう。
  • 他にもデータを追加してみましょう。すべてのダウンロードの平均ダウンロード速度は? ダウンロードに最適なパフォーマンスを得られる時間帯はいつでしょう?
  • アイテムをログから削除したり、ダウンロードが完了したアイテムをすべて削除するボタンを追加してみましょう。
  • 検索機能を追加してみましょう。

関連資料

Storage, nsIDownloadManager, nsIDownload, nsIDownloadProgressListener

ドキュメントのタグと貢献者

Contributors to this page: Mgjbot, Kohei
最終更新者: Mgjbot,