foam-framework
Version:
MVC metaprogramming framework
292 lines (268 loc) • 9.89 kB
JavaScript
/**
* @license
* Copyright 2015 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/
CLASS({
package: 'foam.apps.builder',
name: 'UploadManager',
requires: [
'foam.apps.builder.Identity',
'foam.apps.builder.PackageManager',
'foam.apps.builder.Upload',
'foam.apps.builder.XHRManager',
'foam.dao.EasyDAO',
'foam.metrics.Error',
'foam.metrics.Event',
],
imports: [
'identityManager$',
'metricsDAO',
'sourceManager',
],
constants: {
UPLOAD_TO_CWS: function(id) {
return 'https://www.googleapis.com/upload/chromewebstore/v1.1/items' +
(id ? '/' + id : '');
},
PUBLISH_TO_CWS: function(id) {
return 'https://www.googleapis.com/chromewebstore/v1.1/items/' +
id + '/publish';
},
HEADERS: [
[
/https?:[/][/]www.googleapis.com[/][^?#]*chromewebstore/,
{ 'x-goog-api-version': '2' },
],
],
},
properties: [
{
type: 'foam.apps.builder.ImportExportFlow',
name: 'data',
},
{
type: 'foam.apps.builder.XHRManager',
name: 'xhrManager',
documentation: function() {/* Construct own manager to ensure that
contextual bindings for this upload do not leak into higher-level
XHR management contexts. */},
factory: function() {
return this.XHRManager.create({}, this.Y);
},
},
{
type: 'foam.apps.builder.PackageManager',
name: 'packageManager',
lazyFactory: function() {
return this.PackageManager.create({}, this.Y);
},
},
{
model_: 'foam.core.types.DAOProperty',
name: 'uploadCache',
lazyFactory: function() {
return this.EasyDAO.create({
name: 'uploadCache',
model: this.Upload,
daoType: 'IDB',
cache: true,
logging: true,
}, this.Y);
},
},
],
methods: [
function exportApp(data) {
this.exportApp_(this.completeUpload.bind(this));
},
function exportApp_(ret) {
aseq(
this.sourceManager.aloadSources.bind(this.sourceManager, this.data.config),
this.packageManager.prepareSources.bind(this.packageManager, this.data.config),
apar(
// TODO(markdittmer): We should prompt the user to select an
// upload account here.
this.getIdentity.bind(this),
this.prepareUpload.bind(this)),
this.sendUpload.bind(this),
this.finalizeUpload.bind(this))(ret);
},
function publishApp(data) {
aseq(
apar(
// TODO(markdittmer): We should prompt the user to select an
// upload account here.
this.getIdentity.bind(this),
aseq(
this.getUploadHash.bind(this),
aif(this.needsUpload.bind(this),
this.exportApp_.bind(this),
aconstant(data.config)))),
this.sendPublish.bind(this))
(this.completePublish.bind(this));
},
function getIdentity(ret) { ret(this.identityManager.getIdentity()); },
function getUploadHash(ret) {
this.uploadCache.find(this.data.config.chromeId, {
put: function(obj) { ret(obj); },
error: function() { ret() },
});
},
function needsUpload(hash) {
return ( ! hash ) || this.data.config.hashCode() !== hash.objectHashCode;
},
function sendPublish(ret, identity, config) {
if ( ! this.Identity.isInstance(identity) || ! identity.oauth ) {
this.data.message = 'Oops! Looks like something went wrong.';
this.data.details = this.getOAuthErrorDetails(identity);
this.data.state = 'FAILED';
this.metricsDAO.put(this.Error.create({
name: 'Action:publish:fail - ' +
(config.model_.id || config.name_) +
' - Access denied',
}));
return;
}
// Bind-and-forget. UploadManager and its XHRManager only live for the
// length of one request.
this.xhrManager.bindAuthAgent(
/^https?:[/][/]www[.]googleapis[.]com/, identity.oauth);
this.data.state = 'PUBLISHING';
this.xhrManager.asend(
ret,
this.PUBLISH_TO_CWS(config.chromeId),
JSON.stringify({
// TODO(markdittmer): Change this parameter to "default" when/if we want
// one-click-publish-to-the-world.
target: 'trustedTesters',
}),
'POST');
},
function completePublish(data, xhr, status) {
if ( ( ! status ) || data.error ) {
this.data.message = 'Oops! Something went wrong during your publish request.';
this.data.details = (data && data.error && data.error.errors) ?
// TODO(markdittmer): These may not be internationalized.
data.error.errors.map(function(err) {
return err.message;
}).join(' ') :
'Failed to connect to Chrome Web Store.';
this.data.state = 'FAILED';
this.metricsDAO.put(this.Error.create({
name: 'Action:publish:fail - ' +
(this.data.config.model_.id || this.data.config.name_) +
' - Errors ' + errors.join(' '),
}));
return;
}
this.metricsDAO.put(this.Event.create({
name: 'Action:publish:finish',
label: this.data.config.model_.id || this.data.config.name_,
}));
this.data.state = 'COMPLETED';
this.data.details = 'App published to the Chrome Web Store.';
ret && ret(this.data.config);
},
function prepareUpload(ret, archive) {
var indicators = argsToArray(arguments).slice(2);
if ( ! archive ) {
this.data.message = 'Oops! Looks like something went wrong packaging your app.';
this.data.details = 'Make sure all required fields are filled out.';
this.data.state = 'FAILED';
this.metricsDAO.put(this.Error.create({
name: 'Action:upload:fail - ' +
(this.data.config.model_.id || this.data.config.name_) +
' - Packaging error',
}));
return;
}
var data = new FormData();
data.append('uploadType', 'media');
data.append('fileupload', archive.toBlob());
ret(data);
},
function sendUpload(ret, identity, data) {
var config = this.data.config;
if ( ! this.Identity.isInstance(identity) || ! identity.oauth ) {
this.data.message = 'Oops! Looks like something went wrong.';
this.data.details = this.getOAuthErrorDetails(identity);
this.data.state = 'FAILED';
this.metricsDAO.put(this.Error.create({
name: 'Action:upload:fail - ' +
(config.model_.id || config.name_) +
' - Access denied',
}));
return;
}
// Bind-and-forget. UploadManager and its XHRManager only live for the
// length of one request.
this.xhrManager.bindAuthAgent(
/^https?:[/][/]www[.]googleapis[.]com/, identity.oauth);
this.data.state = 'UPLOADING';
var id = config.chromeId;
this.xhrManager.asend(
ret,
this.UPLOAD_TO_CWS(id),
data,
id ? 'PUT' : 'POST');
},
function finalizeUpload(ret, data, xhr, status) {
if ( ( ! status ) || data.uploadState !== 'SUCCESS' ) {
this.data.message = 'Oops! Something went wrong during the file upload.';
this.data.details = (data && data.itemError) ?
// TODO(markdittmer): These may not be internationalized.
data.itemError.map(function(err) { return err.error_detail; }).join(' ') :
'Failed to connect to Chrome Web Store.';
this.data.state = 'FAILED';
this.metricsDAO.put(this.Error.create({
name: 'Action:upload:fail - ' +
(this.data.config.model_.id || this.data.config.name_) +
' - Status ' + (status ? data.uploadState : xhr.status),
}));
return;
}
this.data.config.chromeId = data.id;
this.uploadCache.put(this.Upload.create({
id: data.id,
objectHashCode: this.data.config.hashCode(),
}, this.Y));
ret && ret(this.data.config);
},
function completeUpload(config) {
this.metricsDAO.put(this.Event.create({
name: 'Action:upload:finish',
label: this.data.config.model_.id || this.data.config.name_,
}));
this.data.state = 'COMPLETED';
this.data.details = 'App uploaded to your Chrome Web Store dashboard.';
},
function getOAuthErrorDetails(err) {
if ( ! err || ! err.message ) {
return "Authentication failed: Either we couldn't reach the " +
"authentication service or you denied the application permission " +
"to upload your app.";
}
// TODO(markdittmer): We should implement a less frail means of detecting
// the type of error received.
if ( err.message.indexOf('request failed') >= 0 ||
err.message.indexOf('Failed to reach') >= 0 ) {
return "Authentication failed: We couldn't reach the authentication " +
"service.";
} else if ( err.message.indexOf('not approve') >= 0 ||
err.message.indexOf('denied') >= 0 ) {
return "Authentication failed: You denied the application permission " +
"to upload your app.";
} else {
return "Authentication failed: Either we couldn't reach the " +
"authentication service or you denied the application permission " +
"to upload your app.";
}
},
],
});