blob: 469904633d9ede041d0575864d075ec62c57cc16 [file] [log] [blame]
// Copyright (c) 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Protocol + host parts of extension URL.
*
* The __FILE_NAME suffix is because the same string constant is used in
* multiple JS files, and JavaScript doesn't have C's #define mechanism (which
* only affects the file its in). Without the suffix, we'd have "constant
* FILE_MANAGER_HOST assigned a value more than once" compiler warnings.
*
* @type {string}
* @const
*/
const FILE_MANAGER_HOST__METADATA_DISPATCHER =
'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
// All of these scripts could be imported with a single call to importScripts,
// but then load and compile time errors would all be reported from the same
// line.
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/metadata_parser.js');
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/byte_reader.js');
/**
* Dispatches metadata requests to the correct parser.
*
* @param {Object} port Worker port.
* @constructor
* @implements {MetadataParserLogger}
* @struct
*/
function MetadataDispatcher(port) {
this.port_ = port;
this.port_.onmessage = this.onMessage.bind(this);
const patterns = [];
this.parserInstances_ = [];
for (let i = 0; i < MetadataDispatcher.parserClasses_.length; i++) {
const parserClass = MetadataDispatcher.parserClasses_[i];
const parser = new parserClass(this);
this.parserInstances_.push(parser);
patterns.push(parser.urlFilter.source);
}
this.parserRegexp_ = new RegExp('(' + patterns.join('|') + ')', 'i');
this.messageHandlers_ = {
init: this.init_.bind(this),
request: this.request_.bind(this)
};
}
/**
* List of registered parser classes.
* @private {!Array<function(new:MetadataParser, !MetadataParserLogger)>}
*/
MetadataDispatcher.parserClasses_ = [];
/**
* Verbose logging for the dispatcher.
*
* Individual parsers also take this as their default verbosity setting.
*/
MetadataDispatcher.prototype.verbose = false;
/**
* |init| message handler.
* @private
*/
MetadataDispatcher.prototype.init_ = function() {
// Inform our owner that we're done initializing.
// If we need to pass more data back, we can add it to the param array.
this.postMessage('initialized', [this.parserRegexp_]);
this.vlog('initialized with URL filter ' + this.parserRegexp_);
};
/**
* |request| message handler.
* @param {string} fileURL File URL.
* @private
*/
MetadataDispatcher.prototype.request_ = function(fileURL) {
try {
this.processOneFile(fileURL, function callback(metadata) {
this.postMessage('result', [fileURL, metadata]);
}.bind(this));
} catch (ex) {
this.error(fileURL, ex);
}
};
/**
* Indicate to the caller that an operation has failed.
*
* No other messages relating to the failed operation should be sent.
* @param {...(Object|string)} var_args Arguments.
*/
MetadataDispatcher.prototype.error = function(var_args) {
const ary = Array.apply(null, arguments);
this.postMessage('error', ary);
};
/**
* Send a log message to the caller.
*
* Callers must not parse log messages for control flow.
* @param {...(Object|string)} var_args Arguments.
*/
MetadataDispatcher.prototype.log = function(var_args) {
const ary = Array.apply(null, arguments);
this.postMessage('log', ary);
};
/**
* Send a log message to the caller only if this.verbose is true.
* @param {...(Object|string)} var_args Arguments.
*/
MetadataDispatcher.prototype.vlog = function(var_args) {
if (this.verbose) {
this.log.apply(this, arguments);
}
};
/**
* Post a properly formatted message to the caller.
* @param {string} verb Message type descriptor.
* @param {Array<Object>} args Arguments array.
*/
MetadataDispatcher.prototype.postMessage = function(verb, args) {
this.port_.postMessage({verb: verb, arguments: args});
};
/**
* Message handler.
* @param {Event} event Event object.
*/
MetadataDispatcher.prototype.onMessage = function(event) {
const data = event.data;
if (this.messageHandlers_.hasOwnProperty(data.verb)) {
this.messageHandlers_[data.verb].apply(this, data.arguments);
} else {
this.log('Unknown message from client: ' + data.verb, data);
}
};
/**
* @param {string} fileURL File URL.
* @param {function(Object)} callback Completion callback.
*/
MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) {
const self = this;
let currentStep = -1;
/**
* @param {...} var_args Arguments.
*/
function nextStep(var_args) {
self.vlog('nextStep: ' + steps[currentStep + 1].name);
steps[++currentStep].apply(self, arguments);
}
let metadata;
/**
* @param {*} err An error.
* @param {string=} opt_stepName Step name.
*/
function onError(err, opt_stepName) {
self.error(
fileURL, opt_stepName || steps[currentStep].name, err.toString(),
metadata);
}
const steps = [
// Step one, find the parser matching the url.
function detectFormat() {
for (let i = 0; i != self.parserInstances_.length; i++) {
const parser = self.parserInstances_[i];
if (fileURL.match(parser.urlFilter)) {
// Create the metadata object as early as possible so that we can
// pass it with the error message.
metadata = parser.createDefaultMetadata();
nextStep(parser);
return;
}
}
onError('unsupported format');
},
// Step two, turn the url into an entry.
function getEntry(parser) {
webkitResolveLocalFileSystemURL(fileURL, entry => {
nextStep(entry, parser);
}, onError);
},
// Step three, turn the entry into a file.
function getFile(entry, parser) {
entry.file(file => {
nextStep(file, parser);
}, onError);
},
// Step four, parse the file content.
function parseContent(file, parser) {
metadata.fileSize = file.size;
try {
parser.parse(file, metadata, callback, onError);
} catch (e) {
onError(e.stack);
}
}
];
nextStep();
};
// Webworker spec says that the worker global object is called self. That's
// a terrible name since we use it all over the chrome codebase to capture
// the 'this' keyword in lambdas.
const global = self;
if (global.constructor.name == 'SharedWorkerGlobalScope') {
global.addEventListener('connect', e => {
const port = e.ports[0];
new MetadataDispatcher(port);
port.start();
});
} else {
// Non-shared worker.
new MetadataDispatcher(global);
}
/**
* @param {function(new:MetadataParser, !MetadataParserLogger)} parserClass
* Parser constructor function.
*/
registerParserClass = parserClass => {
MetadataDispatcher.parserClasses_.push(parserClass);
};
// Note: update component_extension_resources.grd when adding new parsers and
// that these parser scripts imports must be done last, see crbug.com/946959,
// at least after the definition of registerParserClass above.
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/exif_parser.js');
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/image_parsers.js');
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/mpeg_parser.js');
importScripts(
FILE_MANAGER_HOST__METADATA_DISPATCHER +
'/foreground/js/metadata/id3_parser.js');