Home | History | Annotate | Download | only in metadata
      1 // Copyright (c) 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 'use strict';
      6 
      7 /**
      8  * Protocol + host parts of extension URL.
      9  * @type {string}
     10  * @const
     11  */
     12 var FILE_MANAGER_HOST = 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
     13 
     14 // All of these scripts could be imported with a single call to importScripts,
     15 // but then load and compile time errors would all be reported from the same
     16 // line.
     17 importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/metadata_parser.js');
     18 importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/byte_reader.js');
     19 importScripts(FILE_MANAGER_HOST + '/common/js/util.js');
     20 
     21 /**
     22  * Dispatches metadata requests to the correct parser.
     23  *
     24  * @param {Object} port Worker port.
     25  * @constructor
     26  */
     27 function MetadataDispatcher(port) {
     28   this.port_ = port;
     29   this.port_.onmessage = this.onMessage.bind(this);
     30 
     31   // Make sure to update component_extension_resources.grd
     32   // when adding new parsers.
     33   importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/exif_parser.js');
     34   importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/image_parsers.js');
     35   importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/mpeg_parser.js');
     36   importScripts(FILE_MANAGER_HOST + '/foreground/js/metadata/id3_parser.js');
     37 
     38   var patterns = [];
     39 
     40   this.parserInstances_ = [];
     41   for (var i = 0; i < MetadataDispatcher.parserClasses_.length; i++) {
     42     var parserClass = MetadataDispatcher.parserClasses_[i];
     43     var parser = new parserClass(this);
     44     this.parserInstances_.push(parser);
     45     patterns.push(parser.urlFilter.source);
     46   }
     47 
     48   this.parserRegexp_ = new RegExp('(' + patterns.join('|') + ')', 'i');
     49 
     50   this.messageHandlers_ = {
     51     init: this.init_.bind(this),
     52     request: this.request_.bind(this)
     53   };
     54 }
     55 
     56 /**
     57  * List of registered parser classes.
     58  * @private
     59  */
     60 MetadataDispatcher.parserClasses_ = [];
     61 
     62 /**
     63  * @param {function} parserClass Parser constructor function.
     64  */
     65 MetadataDispatcher.registerParserClass = function(parserClass) {
     66   MetadataDispatcher.parserClasses_.push(parserClass);
     67 };
     68 
     69 /**
     70  * Verbose logging for the dispatcher.
     71  *
     72  * Individual parsers also take this as their default verbosity setting.
     73  */
     74 MetadataDispatcher.prototype.verbose = false;
     75 
     76 /**
     77  * |init| message handler.
     78  * @private
     79  */
     80 MetadataDispatcher.prototype.init_ = function() {
     81   // Inform our owner that we're done initializing.
     82   // If we need to pass more data back, we can add it to the param array.
     83   this.postMessage('initialized', [this.parserRegexp_]);
     84   this.log('initialized with URL filter ' + this.parserRegexp_);
     85 };
     86 
     87 /**
     88  * |request| message handler.
     89  * @param {string} fileURL File URL.
     90  * @private
     91  */
     92 MetadataDispatcher.prototype.request_ = function(fileURL) {
     93   try {
     94     this.processOneFile(fileURL, function callback(metadata) {
     95         this.postMessage('result', [fileURL, metadata]);
     96     }.bind(this));
     97   } catch (ex) {
     98     this.error(fileURL, ex);
     99   }
    100 };
    101 
    102 /**
    103  * Indicate to the caller that an operation has failed.
    104  *
    105  * No other messages relating to the failed operation should be sent.
    106  * @param {...Object} var_args Arguments.
    107  */
    108 MetadataDispatcher.prototype.error = function(var_args) {
    109   var ary = Array.apply(null, arguments);
    110   this.postMessage('error', ary);
    111 };
    112 
    113 /**
    114  * Send a log message to the caller.
    115  *
    116  * Callers must not parse log messages for control flow.
    117  * @param {...Object} var_args Arguments.
    118  */
    119 MetadataDispatcher.prototype.log = function(var_args) {
    120   var ary = Array.apply(null, arguments);
    121   this.postMessage('log', ary);
    122 };
    123 
    124 /**
    125  * Send a log message to the caller only if this.verbose is true.
    126  * @param {...Object} var_args Arguments.
    127  */
    128 MetadataDispatcher.prototype.vlog = function(var_args) {
    129   if (this.verbose)
    130     this.log.apply(this, arguments);
    131 };
    132 
    133 /**
    134  * Post a properly formatted message to the caller.
    135  * @param {string} verb Message type descriptor.
    136  * @param {Array.<Object>} args Arguments array.
    137  */
    138 MetadataDispatcher.prototype.postMessage = function(verb, args) {
    139   this.port_.postMessage({verb: verb, arguments: args});
    140 };
    141 
    142 /**
    143  * Message handler.
    144  * @param {Event} event Event object.
    145  */
    146 MetadataDispatcher.prototype.onMessage = function(event) {
    147   var data = event.data;
    148 
    149   if (this.messageHandlers_.hasOwnProperty(data.verb)) {
    150     this.messageHandlers_[data.verb].apply(this, data.arguments);
    151   } else {
    152     this.log('Unknown message from client: ' + data.verb, data);
    153   }
    154 };
    155 
    156 /**
    157  * @param {string} fileURL File URL.
    158  * @param {function(Object)} callback Completion callback.
    159  */
    160 MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) {
    161   var self = this;
    162   var currentStep = -1;
    163 
    164   function nextStep(var_args) {
    165     self.vlog('nextStep: ' + steps[currentStep + 1].name);
    166     steps[++currentStep].apply(self, arguments);
    167   }
    168 
    169   var metadata;
    170 
    171   function onError(err, stepName) {
    172     self.error(fileURL, stepName || steps[currentStep].name, err.toString(),
    173         metadata);
    174   }
    175 
    176   var steps =
    177   [ // Step one, find the parser matching the url.
    178     function detectFormat() {
    179       for (var i = 0; i != self.parserInstances_.length; i++) {
    180         var parser = self.parserInstances_[i];
    181         if (fileURL.match(parser.urlFilter)) {
    182           // Create the metadata object as early as possible so that we can
    183           // pass it with the error message.
    184           metadata = parser.createDefaultMetadata();
    185           nextStep(parser);
    186           return;
    187         }
    188       }
    189       onError('unsupported format');
    190     },
    191 
    192     // Step two, turn the url into an entry.
    193     function getEntry(parser) {
    194       webkitResolveLocalFileSystemURL(
    195           fileURL,
    196           function(entry) { nextStep(entry, parser) },
    197           onError);
    198     },
    199 
    200     // Step three, turn the entry into a file.
    201     function getFile(entry, parser) {
    202       entry.file(function(file) { nextStep(file, parser) }, onError);
    203     },
    204 
    205     // Step four, parse the file content.
    206     function parseContent(file, parser) {
    207       metadata.fileSize = file.size;
    208       try {
    209         parser.parse(file, metadata, callback, onError);
    210       } catch (e) {
    211         onError(e.stack);
    212       }
    213     }
    214   ];
    215 
    216   nextStep();
    217 };
    218 
    219 // Webworker spec says that the worker global object is called self.  That's
    220 // a terrible name since we use it all over the chrome codebase to capture
    221 // the 'this' keyword in lambdas.
    222 var global = self;
    223 
    224 if (global.constructor.name == 'SharedWorkerGlobalScope') {
    225   global.addEventListener('connect', function(e) {
    226     var port = e.ports[0];
    227     new MetadataDispatcher(port);
    228     port.start();
    229   });
    230 } else {
    231   // Non-shared worker.
    232   new MetadataDispatcher(global);
    233 }
    234