Revision 108726 of Monitoring downloads

  • Revision slug: Monitoring_downloads
  • Revision title: Monitoring downloads
  • Revision id: 108726
  • Created:
  • Creator: Sheppy
  • Is current revision? No
  • Comment new

Revision Content

{{template.Fx_minversion_header(3)}} Firefox 3 makes it easier than ever to monitor the status of downloads. Although it was possible to do so in previous versions of Firefox, it was previously only possible for one observer to do so at a time. Firefox 3 introduces new API that allows any number of listeners to observe downloads.

This article demonstrates how to monitor downloads in Firefox 3, using the Download Manager. As a nice bonus, it also demonstrates how to use the Storage API to issue sqlite commands on a database. The result is a window you can open by choosing "Download log" in the Tools menu, which lists all downloads that have been started since you installed the extension. In the list is the name of the file, the start and end times of the download, the download speed, and the status of the download. A tooltip is included that displays the full source URL of the file.

add download link for complete code here

Setting up

When the extension loads, it will do some housekeeping chores. In particular, it needs to get an instance of the Download Manager's nsIDownloadManager interface and create the database into which its data will be stored.

 onLoad: function() {
   // initialization code
   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);
   
   // Open the database, placing its file in the profile directory
   
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   
   // Get access to the storage service and open the database
   
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   
   var dbConn = this.storageService.openDatabase(this.dbFile);
       
   // Now create the table; if it already exists, this fails, but we don't care!
   
   dbConn.executeSimpleSQL("CREATE TABLE items (source TEXT, size INTEGER, startTime INTEGER, endTime INTEGER, speed REAL, status INTEGER)");
   dbConn.close();
 },

This is fairly simple stuff. The Download Manager instance is cached into a member variable in the downloadlogger object for reuse later, and its addListener() method is called to start listening for download status updates. The database file is opened, and an sqlite CREATE TABLE command is executed to create the table.

Finally, the database is closed.

Note: The mozIStorageConnection method close() is being added to Firefox 3.0a8; in prior versions of Firefox, there is no way to explicitly close the database. Instead, it is closed when the garbage collector disposes of the connection object.

Handling download state changes

Once the code above is run, our onDownloadStateChange() method is called whenever a download's state changes. This is part of the nsIDownloadProgressListener interface.

That code looks like this:

 onDownloadStateChange: function(aState, aDownload) {
   var statement;
   
   switch(aDownload.state) {
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
     
       // Add a new row for the download being started; each row includes the source URI, size,
       // and start time.  The end time and download speed are both set to 0 at first, since we
       // don't know those yet.
       
       // status is the same status value that came from the download manager.
       
       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;
       
     // Record the completion (whether failed or successful) of the download
 
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FAILED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_CANCELED:
       this.logTransferCompleted(aDownload);
       break;
   }
 },

We're interested in four states. If the download's state, indicated by the aDownload.state field, is Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING, the file has begun to download. The aDownload object is an nsIDownload object.

In that case, we create a new row in our database for the new file by opening the database and building a REPLACE INTO sqlite command. The first three rows are set to the values of the source URI, file size, and start time fields from the download object. The remaining rows are set to zeroes since that's not information we have at the moment.

If the download's state indicates that the download is finished, canceled, or failed, we call our logTransferCompleted routine to update the log to indicate that state change. That code looks like this:

 logTransferCompleted: function(aDownload) {
     var endTime = new Date();                     // Current time is the end time
     
     // Issue the REPLACE sqlite command to update the record.  We find a record for the same
     // source URI and start time, then update the end time, size, and speed entries in the
     // record.  By matching on both source URI and start time, we support logging multiple
     // downloads of the same file.
     
     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();
 },

This simply opens the database and builds and executes a UPDATE sqlite command that finds the download item whose source URI and start time match the download that has completed and updates its information. By looking for a record with both the same URI and start time, we properly support the case where the user downloads the same file multiple times.

Exercises for the reader

There are some obvious things that could be done to improve this extension. If you're learning to use the Download Manager or Storage APIs, they're things you might look into doing for practice:

  • Add code to update the download log window on the fly, instead of generating a static list when it's first opened.
  • Add additional statistics. What's the average download speed across all downloads? What times of day do you get the best download performance?
  • Add buttons to delete items from the log, or to delete all items that have finished downloading.
  • Add searching.

See also

Storage, nsIDownloadManager, nsIDownload, nsIDownloadProgressListener

Revision Source

<p>{{template.Fx_minversion_header(3)}}
Firefox 3 makes it easier than ever to monitor the status of downloads.  Although it was possible to do so in previous versions of Firefox, it was previously only possible for one observer to do so at a time.  Firefox 3 introduces new API that allows any number of listeners to observe downloads.
</p><p>This article demonstrates how to monitor downloads in Firefox 3, using the Download Manager.  As a nice bonus, it also demonstrates how to use the <a href="en/Storage">Storage</a> API to issue <a class="external" href="http://www.sqlite.org/">sqlite</a> commands on a database.  The result is a window you can open by choosing "Download log" in the Tools menu, which lists all downloads that have been started since you installed the extension.  In the list is the name of the file, the start and end times of the download, the download speed, and the status of the download.  A tooltip is included that displays the full source URL of the file.
</p><p><b>add download link for complete code here</b>
</p>
<h3 name="Setting_up">Setting up</h3>
<p>When the extension loads, it will do some housekeeping chores.  In particular, it needs to get an instance of the Download Manager's <code><a href="en/NsIDownloadManager">nsIDownloadManager</a></code> interface and create the database into which its data will be stored.
</p>
<pre class="eval"> onLoad: function() {
   // initialization code
   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);
   
   // Open the database, placing its file in the profile directory
   
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   
   // Get access to the storage service and open the database
   
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   
   var dbConn = this.storageService.openDatabase(this.dbFile);
       
   // Now create the table; if it already exists, this fails, but we don't care!
   
   dbConn.executeSimpleSQL("CREATE TABLE items (source TEXT, size INTEGER, startTime INTEGER, endTime INTEGER, speed REAL, status INTEGER)");
   dbConn.close();
 },
</pre>
<p>This is fairly simple stuff.  The Download Manager instance is cached into a member variable in the <code>downloadlogger</code> object for reuse later, and its <code>addListener()</code> method is called to start listening for download status updates.  The database file is opened, and an sqlite <code>CREATE TABLE</code> command is executed to create the table.
</p><p>Finally, the database is closed.
</p>
<div class="note"><b>Note:</b> The <code><a href="en/MozIStorageConnection">mozIStorageConnection</a></code> method <code>close()</code> is being added to Firefox 3.0a8; in prior versions of Firefox, there is no way to explicitly close the database.  Instead, it is closed when the garbage collector disposes of the connection object.</div>
<h3 name="Handling_download_state_changes">Handling download state changes</h3>
<p>Once the code above is run, our <code>onDownloadStateChange()</code> method is called whenever a download's state changes.  This is part of the <code><a href="en/NsIDownloadProgressListener">nsIDownloadProgressListener</a></code> interface.
</p><p>That code looks like this:
</p>
<pre class="eval"> onDownloadStateChange: function(aState, aDownload) {
   var statement;
   
   switch(aDownload.state) {
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
     
       // Add a new row for the download being started; each row includes the source URI, size,
       // and start time.  The end time and download speed are both set to 0 at first, since we
       // don't know those yet.
       
       // status is the same status value that came from the download manager.
       
       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;
       
     // Record the completion (whether failed or successful) of the download
 
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FAILED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_CANCELED:
       this.logTransferCompleted(aDownload);
       break;
   }
 },
</pre>
<p>We're interested in four states.  If the download's state, indicated by the <code>aDownload.state</code> field, is <code>Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING</code>, the file has begun to download.  The <code>aDownload</code> object is an <code><a href="en/NsIDownload">nsIDownload</a></code> object.
</p><p>In that case, we create a new row in our database for the new file by opening the database and building a <code>REPLACE INTO</code> sqlite command.  The first three rows are set to the values of the source URI, file size, and start time fields from the download object.  The remaining rows are set to zeroes since that's not information we have at the moment.
</p><p>If the download's state indicates that the download is finished, canceled, or failed, we call our <code>logTransferCompleted</code> routine to update the log to indicate that state change.  That code looks like this:
</p>
<pre class="eval"> logTransferCompleted: function(aDownload) {
     var endTime = new Date();                     // Current time is the end time
     
     // Issue the REPLACE sqlite command to update the record.  We find a record for the same
     // source URI and start time, then update the end time, size, and speed entries in the
     // record.  By matching on both source URI and start time, we support logging multiple
     // downloads of the same file.
     
     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();
 },
</pre>
<p>This simply opens the database and builds and executes a <code>UPDATE</code> sqlite command that finds the download item whose source URI and start time match the download that has completed and updates its information.  By looking for a record with both the same URI and start time, we properly support the case where the user downloads the same file multiple times.
</p>
<h3 name="Exercises_for_the_reader">Exercises for the reader</h3>
<p>There are some obvious things that could be done to improve this extension.  If you're learning to use the Download Manager or Storage APIs, they're things you might look into doing for practice:
</p>
<ul><li> Add code to update the download log window on the fly, instead of generating a static list when it's first opened.
</li></ul>
<ul><li> Add additional statistics.  What's the average download speed across all downloads?  What times of day do you get the best download performance?
</li></ul>
<ul><li> Add buttons to delete items from the log, or to delete all items that have finished downloading.
</li></ul>
<ul><li> Add searching.
</li></ul>
<h3 name="See_also">See also</h3>
<p><a href="en/Storage">Storage</a>, <code><a href="en/NsIDownloadManager">nsIDownloadManager</a></code>, <code><a href="en/NsIDownload">nsIDownload</a></code>, <code><a href="en/NsIDownloadProgressListener">nsIDownloadProgressListener</a></code>
</p>
Revert to this revision