You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by yu...@apache.org on 2013/06/26 23:18:30 UTC

svn commit: r1497102 - in /incubator/ambari/branches/branch-1.2.5/ambari-web: ./ app/ app/controllers/main/admin/security/add/ app/data/ app/styles/ app/utils/ vendor/scripts/

Author: yusaku
Date: Wed Jun 26 21:18:30 2013
New Revision: 1497102

URL: http://svn.apache.org/r1497102
Log:
AMBARI-2491. Security Wizard: show which principals and keytabs need to be created on which hosts. (Andrii Tkach via yusaku)

Added:
    incubator/ambari/branches/branch-1.2.5/ambari-web/vendor/scripts/FileSaver.js
Modified:
    incubator/ambari/branches/branch-1.2.5/ambari-web/app/controllers/main/admin/security/add/step2.js
    incubator/ambari/branches/branch-1.2.5/ambari-web/app/data/secure_properties.js
    incubator/ambari/branches/branch-1.2.5/ambari-web/app/messages.js
    incubator/ambari/branches/branch-1.2.5/ambari-web/app/styles/application.less
    incubator/ambari/branches/branch-1.2.5/ambari-web/app/utils/string_utils.js
    incubator/ambari/branches/branch-1.2.5/ambari-web/config.coffee

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/app/controllers/main/admin/security/add/step2.js
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/app/controllers/main/admin/security/add/step2.js?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/app/controllers/main/admin/security/add/step2.js (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/app/controllers/main/admin/security/add/step2.js Wed Jun 26 21:18:30 2013
@@ -17,6 +17,8 @@
  */
 
 var App = require('app');
+var stringUtils = require('utils/string_utils');
+
 App.MainAdminSecurityAddStep2Controller = Em.Controller.extend({
 
   name: 'mainAdminSecurityAddStep2Controller',
@@ -150,13 +152,110 @@ App.MainAdminSecurityAddStep2Controller 
     }
   },
 
+  showHostPrincipalKeytabList: function(){
+    App.ModalPopup.show({
+      self: this,
+      header: Em.I18n.t('admin.security.step2.popup.header'),
+      primary: Em.I18n.t('common.proceed'),
+      downloadCsv: Em.I18n.t('admin.security.step2.popup.downloadCSV'),
+      classNames: ['sixty-percent-width-modal'],
+      csvContent: [],
+      onDownloadCsv: function(){
+        var blob = new Blob([this.get('csvContent')], {type: "text/csv;charset=utf-8"});
+        saveAs(blob, "host-principal-keytab-list.csv");
+      },
+      onPrimary: function(){
+        this.hide();
+        App.router.send('next');
+      },
+      buildCsvContent: function(data){
+        this.set('csvContent', stringUtils.arrayToCSV(data));
+      },
+      bodyClass: Em.View.extend({
+        componentsToDisplay: ['NAMENODE', 'SECONDARY_NAMENODE', 'DATANODE', 'JOBTRACKER', 'ZOOKEEPER_SERVER', 'HIVE_SERVER', 'TASKTRACKER',
+        'OOZIE_SERVER', 'NAGIOS_SERVER', 'HBASE_MASTER', 'HBASE_REGIONSERVER'],
+        hostComponents: function(){
+          var componentsToDisplay = this.get('componentsToDisplay');
+          var configs = this.get('parentView.self.stepConfigs');
+          var hosts = App.Host.find();
+          var result = [];
+          hosts.forEach(function(host){
+            host.get('hostComponents').forEach(function(hostComponent){
+              if(componentsToDisplay.contains(hostComponent.get('componentName'))){
+                var serviceConfigs = configs.findProperty('serviceName', hostComponent.get('service.serviceName')).get('configs');
+                var principal, keytab;
+                serviceConfigs.forEach(function(config){
+                  if (config.get('component') && config.get('component') === hostComponent.get('componentName')) {
+                    if (config.get('name').substr(-15, 15) === '_principal_name') {
+                      principal = config.get('value').replace('_HOST', host.get('hostName')) + config.get('unit');
+                    } else if (config.get('name').substr(-7, 7) === '_keytab' || config.get('name').substr(-12, 12) === '_keytab_path') {
+                      keytab = config.get('value');
+                    }
+                  } else if (config.get('components') && config.get('components').contains(hostComponent.get('componentName'))) {
+                    if (config.get('name').substr(-15, 15) === '_principal_name') {
+                      principal = config.get('value').replace('_HOST', host.get('hostName')) + config.get('unit');
+                    } else if (config.get('name').substr(-7, 7) === '_keytab' || config.get('name').substr(-12, 12) === '_keytab_path') {
+                      keytab = config.get('value');
+                    }
+                  }
+                });
+
+                result.push({
+                  host: host.get('hostName'),
+                  component: hostComponent.get('displayName'),
+                  principal: principal,
+                  keytab: keytab
+                });
+              }
+            });
+          });
+          this.get('parentView').buildCsvContent(result);
+          return result;
+        }.property(),
+        template: Em.Handlebars.compile([
+          '<div class="alert alert-info">{{t admin.security.step2.popup.notice}}</div>',
+          '<div class="long-popup-list">',
+            '<table class="table table-bordered table-striped">',
+            '<thead>',
+              '<tr>',
+                '<th>{{t common.host}}</th>',
+                '<th>{{t common.component}}</th>',
+                '<th>{{t admin.security.step2.popup.table.principal}}</th>',
+                '<th>{{t admin.security.step2.popup.table.keytab}}</th>',
+              '</tr>',
+            '</thead>',
+            '<tbody>',
+            '{{#each hostComponent in view.hostComponents}}',
+              '<tr>',
+                '<td>{{hostComponent.host}}</td>',
+                '<td>{{hostComponent.component}}</td>',
+                '<td>{{hostComponent.principal}}</td>',
+                '<td>{{hostComponent.keytab}}</td>',
+              '</tr>',
+            '{{/each}}',
+            '</tbody>',
+            '</table>',
+          '</div>'
+        ].join(''))
+      }),
+      footerClass: Em.View.extend({
+        classNames: ['modal-footer'],
+        template: Em.Handlebars.compile([
+          '{{#if view.parentView.downloadCsv}}<a class="btn btn-info" {{action onDownloadCsv target="view.parentView"}}>{{view.parentView.downloadCsv}}</a>&nbsp;{{/if}}',
+          '{{#if view.parentView.secondary}}<a class="btn" {{action onSecondary target="view.parentView"}}>{{view.parentView.secondary}}</a>&nbsp;{{/if}}',
+          '{{#if view.parentView.primary}}<a class="btn btn-success" {{action onPrimary target="view.parentView"}}>{{view.parentView.primary}}</a>{{/if}}'
+        ].join(''))
+      })
+    });
+  },
+
   /**
    *  submit and move to step3
    */
 
   submit: function () {
     if (!this.get('isSubmitDisabled')) {
-      App.router.send('next');
+      this.showHostPrincipalKeytabList();
     }
   }
 

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/app/data/secure_properties.js
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/app/data/secure_properties.js?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/app/data/secure_properties.js (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/app/data/secure_properties.js Wed Jun 26 21:18:30 2013
@@ -159,7 +159,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HDFS",
-      "category": "NameNode"
+      "category": "NameNode",
+      "components": ["NAMENODE", "SECONDARY_NAMENODE"]
     },
     {
       "id": "puppet var",
@@ -172,7 +173,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HDFS",
-      "category": "NameNode"
+      "category": "NameNode",
+      "components": ["NAMENODE", "SECONDARY_NAMENODE"]
     },
     {
       "id": "puppet var",
@@ -211,7 +213,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HDFS",
-      "category": "DataNode"
+      "category": "DataNode",
+      "component": "DATANODE"
     },
     {
       "id": "puppet var",
@@ -224,7 +227,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HDFS",
-      "category": "DataNode"
+      "category": "DataNode",
+      "component": "DATANODE"
     },
     //MAPREDUCE
     {
@@ -238,7 +242,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "MAPREDUCE",
-      "category": "JobTracker"
+      "category": "JobTracker",
+      "component": "JOBTRACKER"
     },
     {
       "id": "puppet var",
@@ -251,7 +256,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "MAPREDUCE",
-      "category": "JobTracker"
+      "category": "JobTracker",
+      "component": "JOBTRACKER"
     },
     {
       "id": "puppet var",
@@ -264,7 +270,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "MAPREDUCE",
-      "category": "TaskTracker"
+      "category": "TaskTracker",
+      "component": "TASKTRACKER"
     },
     {
       "id": "puppet var",
@@ -277,7 +284,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "MAPREDUCE",
-      "category": "TaskTracker"
+      "category": "TaskTracker",
+      "component": "TASKTRACKER"
     },
 
     //HBASE
@@ -292,7 +300,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HBASE",
-      "category": "HBase"
+      "category": "HBase",
+      "components": ["HBASE_MASTER", "HBASE_REGIONSERVER"]
     },
     {
       "id": "puppet var",
@@ -305,7 +314,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HBASE",
-      "category": "HBase"
+      "category": "HBase",
+      "components": ["HBASE_MASTER", "HBASE_REGIONSERVER"]
     },
 
     //HIVE
@@ -333,7 +343,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HIVE",
-      "category": "Hive Metastore"
+      "category": "Hive Metastore",
+      "component": "HIVE_SERVER"
     },
     {
       "id": "puppet var",
@@ -346,7 +357,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "HIVE",
-      "category": "Hive Metastore"
+      "category": "Hive Metastore",
+      "component": "HIVE_SERVER"
 
     },
 
@@ -375,7 +387,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "OOZIE",
-      "category": "Oozie Server"
+      "category": "Oozie Server",
+      "component": "OOZIE_SERVER"
     },
     {
       "id": "puppet var",
@@ -388,7 +401,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "OOZIE",
-      "category": "Oozie Server"
+      "category": "Oozie Server",
+      "component": "OOZIE_SERVER"
     },
 
     //ZooKeeper
@@ -403,7 +417,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "ZOOKEEPER",
-      "category": "ZooKeeper Server"
+      "category": "ZooKeeper Server",
+      "component": "ZOOKEEPER_SERVER"
     },
     {
       "id": "puppet var",
@@ -416,7 +431,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "ZOOKEEPER",
-      "category": "ZooKeeper Server"
+      "category": "ZooKeeper Server",
+      "component": "ZOOKEEPER_SERVER"
     },
     //NAGIOS
     {
@@ -443,7 +459,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "NAGIOS",
-      "category": "Nagios Server"
+      "category": "Nagios Server",
+      "component": "NAGIOS_SERVER"
     },
     {
       "id": "puppet var",
@@ -456,7 +473,8 @@ module.exports =
       "isVisible": true,
       "isOverridable": false,
       "serviceName": "NAGIOS",
-      "category": "Nagios Server"
+      "category": "Nagios Server",
+      "component": "NAGIOS_SERVER"
     }
 
   ]

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/app/messages.js
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/app/messages.js?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/app/messages.js (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/app/messages.js Wed Jun 26 21:18:30 2013
@@ -89,6 +89,7 @@ Em.I18n.translations = {
   'common.diskUsage':'Disk Usage',
   'common.loadAvg':'Load Avg',
   'common.components':'Components',
+  'common.component':'Component',
   'common.quickLinks':'Quick Links',
   'common.save':'Save',
   'common.servers':'Servers',
@@ -135,6 +136,7 @@ Em.I18n.translations = {
   'common.use': 'Use',
   'common.stacks': 'Stacks',
   'common.reset': 'Reset',
+  'common.proceed': 'Proceed',
 
   'requestInfo.installComponents':'Install Components',
   'requestInfo.installServices':'Install Services',
@@ -601,6 +603,13 @@ Em.I18n.translations = {
   'admin.security.step1.body.instruction3': 'Create Kerberos principals for Hadoop services and hosts',
   'admin.security.step1.body.instruction4': 'Generate keytabs for each principal and place on the appropriate hosts',
   'admin.security.step2.body.header': 'Configure Kerberos security properties',
+  'admin.security.step2.popup.header': 'Manual Steps Required Before Proceeding',
+  'admin.security.step2.popup.notice': 'You need to create the following principals and keytabs on the hosts shown.<br />'+
+  'You can download the list as a CSV file and use it to create a script to generate the principals and keytabs.' +
+  'Once the principals and keytabs have been created, click on <i>Proceed</i> to continue. If you need to make configuration changes, click <i>Cancel</i>.',
+  'admin.security.step2.popup.table.principal': 'Principal',
+  'admin.security.step2.popup.table.keytab': 'Keytab',
+  'admin.security.step2.popup.downloadCSV': 'Download CSV',
   'admin.security.step3.body.header': 'Applying kerberos security to the cluster',
   'admin.security.step3.body.success.header' : 'Kerberos-based security has been enabled on your cluster. Please wait while services are started in secure mode.',
   'admin.security.step3.body.failure.header' : 'Failed to enable Kerberos-based security on your cluster. Your cluster will keep running in non-secure mode.',

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/app/styles/application.less
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/app/styles/application.less?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/app/styles/application.less (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/app/styles/application.less Wed Jun 26 21:18:30 2013
@@ -935,6 +935,11 @@ a:focus {
     max-height: 544px;
     top: 5%;
   }
+  .long-popup-list {
+    height: 280px;
+    overflow: auto;
+    width: 100%;
+  }
 
   .modal-body {
 

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/app/utils/string_utils.js
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/app/utils/string_utils.js?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/app/utils/string_utils.js (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/app/utils/string_utils.js Wed Jun 26 21:18:30 2013
@@ -105,5 +105,23 @@ module.exports = {
 
   isSingleLine: function(string){
     return string.trim().indexOf("\n") == -1;
+  },
+  /**
+   * transform array of objects into CSV format content
+   * @param array
+   * @return {Array}
+   */
+  arrayToCSV: function(array){
+    var content = "";
+    array.forEach(function(item){
+      var row = [];
+      for(var i in item){
+        if(item.hasOwnProperty(i)){
+          row.push(item[i]);
+        }
+      }
+      content += row.join(',') + '\n';
+    });
+    return content;
   }
 }

Modified: incubator/ambari/branches/branch-1.2.5/ambari-web/config.coffee
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/config.coffee?rev=1497102&r1=1497101&r2=1497102&view=diff
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/config.coffee (original)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/config.coffee Wed Jun 26 21:18:30 2013
@@ -55,7 +55,8 @@ exports.config =
           'vendor/scripts/workflow_visualization.js',
           'vendor/scripts/rickshaw.js',
           'vendor/scripts/spin.js',
-          'vendor/scripts/jquery.flexibleArea.js'
+          'vendor/scripts/jquery.flexibleArea.js',
+          'vendor/scripts/FileSaver.js'
           ]
 
     stylesheets:

Added: incubator/ambari/branches/branch-1.2.5/ambari-web/vendor/scripts/FileSaver.js
URL: http://svn.apache.org/viewvc/incubator/ambari/branches/branch-1.2.5/ambari-web/vendor/scripts/FileSaver.js?rev=1497102&view=auto
==============================================================================
--- incubator/ambari/branches/branch-1.2.5/ambari-web/vendor/scripts/FileSaver.js (added)
+++ incubator/ambari/branches/branch-1.2.5/ambari-web/vendor/scripts/FileSaver.js Wed Jun 26 21:18:30 2013
@@ -0,0 +1,216 @@
+/* FileSaver.js
+ * A saveAs() FileSaver implementation.
+ * 2013-01-23
+ *
+ * By Eli Grey, http://eligrey.com
+ * License: X11/MIT
+ *   See LICENSE.md
+ */
+
+/*global self */
+/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true,
+  plusplus: true */
+
+/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */
+
+var saveAs = saveAs
+  || (navigator.msSaveBlob && navigator.msSaveBlob.bind(navigator))
+  || (function(view) {
+	"use strict";
+	var
+		  doc = view.document
+		  // only get URL when necessary in case BlobBuilder.js hasn't overridden it yet
+		, get_URL = function() {
+			return view.URL || view.webkitURL || view;
+		}
+		, URL = view.URL || view.webkitURL || view
+		, save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
+		, can_use_save_link = "download" in save_link
+		, click = function(node) {
+			var event = doc.createEvent("MouseEvents");
+			event.initMouseEvent(
+				"click", true, false, view, 0, 0, 0, 0, 0
+				, false, false, false, false, 0, null
+			);
+			node.dispatchEvent(event);
+		}
+		, webkit_req_fs = view.webkitRequestFileSystem
+		, req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
+		, throw_outside = function (ex) {
+			(view.setImmediate || view.setTimeout)(function() {
+				throw ex;
+			}, 0);
+		}
+		, force_saveable_type = "application/octet-stream"
+		, fs_min_size = 0
+		, deletion_queue = []
+		, process_deletion_queue = function() {
+			var i = deletion_queue.length;
+			while (i--) {
+				var file = deletion_queue[i];
+				if (typeof file === "string") { // file is an object URL
+					URL.revokeObjectURL(file);
+				} else { // file is a File
+					file.remove();
+				}
+			}
+			deletion_queue.length = 0; // clear queue
+		}
+		, dispatch = function(filesaver, event_types, event) {
+			event_types = [].concat(event_types);
+			var i = event_types.length;
+			while (i--) {
+				var listener = filesaver["on" + event_types[i]];
+				if (typeof listener === "function") {
+					try {
+						listener.call(filesaver, event || filesaver);
+					} catch (ex) {
+						throw_outside(ex);
+					}
+				}
+			}
+		}
+		, FileSaver = function(blob, name) {
+			// First try a.download, then web filesystem, then object URLs
+			var
+				  filesaver = this
+				, type = blob.type
+				, blob_changed = false
+				, object_url
+				, target_view
+				, get_object_url = function() {
+					var object_url = get_URL().createObjectURL(blob);
+					deletion_queue.push(object_url);
+					return object_url;
+				}
+				, dispatch_all = function() {
+					dispatch(filesaver, "writestart progress write writeend".split(" "));
+				}
+				// on any filesys errors revert to saving with object URLs
+				, fs_error = function() {
+					// don't create more object URLs than needed
+					if (blob_changed || !object_url) {
+						object_url = get_object_url(blob);
+					}
+					if (target_view) {
+						target_view.location.href = object_url;
+					} else {
+                        window.open(object_url, "_blank");
+                    }
+					filesaver.readyState = filesaver.DONE;
+					dispatch_all();
+				}
+				, abortable = function(func) {
+					return function() {
+						if (filesaver.readyState !== filesaver.DONE) {
+							return func.apply(this, arguments);
+						}
+					};
+				}
+				, create_if_not_found = {create: true, exclusive: false}
+				, slice
+			;
+			filesaver.readyState = filesaver.INIT;
+			if (!name) {
+				name = "download";
+			}
+			if (can_use_save_link) {
+				object_url = get_object_url(blob);
+				save_link.href = object_url;
+				save_link.download = name;
+				click(save_link);
+				filesaver.readyState = filesaver.DONE;
+				dispatch_all();
+				return;
+			}
+			// Object and web filesystem URLs have a problem saving in Google Chrome when
+			// viewed in a tab, so I force save with application/octet-stream
+			// http://code.google.com/p/chromium/issues/detail?id=91158
+			if (view.chrome && type && type !== force_saveable_type) {
+				slice = blob.slice || blob.webkitSlice;
+				blob = slice.call(blob, 0, blob.size, force_saveable_type);
+				blob_changed = true;
+			}
+			// Since I can't be sure that the guessed media type will trigger a download
+			// in WebKit, I append .download to the filename.
+			// https://bugs.webkit.org/show_bug.cgi?id=65440
+			if (webkit_req_fs && name !== "download") {
+				name += ".download";
+			}
+			if (type === force_saveable_type || webkit_req_fs) {
+				target_view = view;
+			}
+			if (!req_fs) {
+				fs_error();
+				return;
+			}
+			fs_min_size += blob.size;
+			req_fs(view.TEMPORARY, fs_min_size, abortable(function(fs) {
+				fs.root.getDirectory("saved", create_if_not_found, abortable(function(dir) {
+					var save = function() {
+						dir.getFile(name, create_if_not_found, abortable(function(file) {
+							file.createWriter(abortable(function(writer) {
+								writer.onwriteend = function(event) {
+									target_view.location.href = file.toURL();
+									deletion_queue.push(file);
+									filesaver.readyState = filesaver.DONE;
+									dispatch(filesaver, "writeend", event);
+								};
+								writer.onerror = function() {
+									var error = writer.error;
+									if (error.code !== error.ABORT_ERR) {
+										fs_error();
+									}
+								};
+								"writestart progress write abort".split(" ").forEach(function(event) {
+									writer["on" + event] = filesaver["on" + event];
+								});
+								writer.write(blob);
+								filesaver.abort = function() {
+									writer.abort();
+									filesaver.readyState = filesaver.DONE;
+								};
+								filesaver.readyState = filesaver.WRITING;
+							}), fs_error);
+						}), fs_error);
+					};
+					dir.getFile(name, {create: false}, abortable(function(file) {
+						// delete file if it already exists
+						file.remove();
+						save();
+					}), abortable(function(ex) {
+						if (ex.code === ex.NOT_FOUND_ERR) {
+							save();
+						} else {
+							fs_error();
+						}
+					}));
+				}), fs_error);
+			}), fs_error);
+		}
+		, FS_proto = FileSaver.prototype
+		, saveAs = function(blob, name) {
+			return new FileSaver(blob, name);
+		}
+	;
+	FS_proto.abort = function() {
+		var filesaver = this;
+		filesaver.readyState = filesaver.DONE;
+		dispatch(filesaver, "abort");
+	};
+	FS_proto.readyState = FS_proto.INIT = 0;
+	FS_proto.WRITING = 1;
+	FS_proto.DONE = 2;
+
+	FS_proto.error =
+	FS_proto.onwritestart =
+	FS_proto.onprogress =
+	FS_proto.onwrite =
+	FS_proto.onabort =
+	FS_proto.onerror =
+	FS_proto.onwriteend =
+		null;
+
+	view.addEventListener("unload", process_deletion_queue, false);
+	return saveAs;
+}(self));