<commit_before>;(function(){//IFEE

angular.module('brewKeeper')
      .controller('recipeDetail', function($scope, $http, $location, $routeParams, $rootScope){
          var id = $routeParams.id;
          var username = $routeParams.username;
          $scope.username = $routeParams.username;
          $scope.id = $routeParams.id;
          $(document).scrollTop(0);

          $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
            .then(function(response){
              $rootScope.detail = response.data;
              $rootScope.steps = response.data.steps;
              $rootScope.notes = response.data.brewnotes;
              var currentRating = $rootScope.detail.rating;
              $scope.rating = 0;
              $scope.ratings = [{
                  current: currentRating,
                  max: 5
              }];

          }) //end http.get

          $scope.rateRecipe = function (rating) {
            var newRating = {"rating": rating}
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/", newRating)
          }

          $scope.EliminateRecipe = function() {
            if (window.confirm("Are you sure you want to delete " + $scope.detail.title + "?")){
              $http.delete('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/').then(function(){
                $location.path('/'+ username);
              })
            };
          }; //end Eliminate function

          $scope.deleteStep = function(stepNumber, stepId){
            if (window.confirm("Are you sure you want to delete step " + stepNumber + "?")){
              $http.delete("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ stepId + "/").then(function(){
                $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id +'/')
                  .then(function(response){
                    $scope.steps = response.data.steps;
                    if(response.data.steps.length == 0){
                      $(".brew-it-button").addClass("hidden");
                      $(".no-steps").removeClass("hidden");
                    }
                  })
              })
            }
          } //end deleteStep function

          // $scope.showSteps = function(stepId){
          //   stepId= "div." + stepId.toString()
          //   $(stepId).removeClass("hidden")
          // };

          $scope.hideEditStep = function(stepId){
            stepId = "div." + stepId.toString();
            $(stepId).toggleClass("hidden")
          }

          $scope.editStep = function(step){
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step)
              .then(function() {
                $scope.hideEditStep(step.id)
                // $scope.showEditSteps(step.id)
              });
          } //end editStep function

          $scope.showEditStep = function(stepId){
            stepId = "div." + stepId.toString();
            $(stepId).toggleClass("hidden");
          }

          $scope.increaseStep = function(step){
            if(step.step_number >= $rootScope.steps.length){
              return
            }
            step.step_number++
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $rootScope.steps = response.data.steps;
                })
            })
          } //end increaseStep function

          $scope.decreaseStep = function(step){
            if(step.step_number <= 1){
              return
            }
            step.step_number--
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $rootScope.steps = response.data.steps;
                })
            })
          } //end decreaseStep function

          $scope.step = { }//Might need to prepopulate this with empty strings for each key... Maybe...
          $scope.addStep=function(){ //add step function
            $scope.step.step_number = $scope.steps.length + 1;
            $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/", $scope.step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $(".brew-it-button").removeClass("hidden");
                  $(".no-steps").addClass("hidden");
                  $rootScope.steps = response.data.steps;
                })
            })
            $scope.step= { };
          } //end new step submit function

        $scope.showAddSteps = function(){
          $("form.create-new-step").toggleClass("hidden");
        };//Reveal "Add Step" when new recipe form is submitted.
        // $scope.hideAddSteps = function(){
        //   $("form.create-new-step").addClass("hidden");
        //   $("button.done-adding").addClass("hidden");
        // }; //hides the add step form

        $('.edit-button').on('click', function(){
          $('.edit-recipe').removeClass("hidden");
          $('.recipe-view').addClass("hidden");
        });

        $(".no-steps").click(function(){
          $scope.showAddSteps()
        })

      $scope.editRecipe = function(recipe){
        $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/", recipe)
        .then( function () {
          $('.edit-recipe').addClass("hidden");
          $('.recipe-view').removeClass("hidden");
        })
      } //end editRecipe function
        $('.cancel-recipe-edit').on('click', function(){
          $('.recipe-view').removeClass("hidden");
          $('.edit-recipe').addClass("hidden");
        });

        // Function for cloning recipes
        $scope.cloneRecipe = function(){
          if (!window.confirm("Are you sure you want to clone "+ $scope.detail.title +" ?")){
            return;
          };
          var cloneData = {}
          cloneData.title = "Clone of: " + $scope.detail.title;
          cloneData.bean_name = $scope.detail.bean_name;
          cloneData.roast = $scope.detail.roast;
          cloneData.orientation = $scope.detail.orientation;
          cloneData.general_recipe_comment = $scope.detail.general_recipe_comment;
          cloneData.grind = $scope.detail.grind;
          cloneData.total_bean_amount = $scope.detail.total_bean_amount;
          cloneData.bean_units = $scope.detail.bean_units;
          cloneData.water_type = $scope.detail.water_type;
          cloneData.total_water_amount = $scope.detail.total_water_amount;
          cloneData.water_units = $scope.detail.water_units;
          cloneData.temp = $scope.detail.temp;
          cloneData.steps = [];

          $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/", cloneData).success(function(response){

            newRecipeId = response.id;
            steps = [];
            for(step in $scope.detail.steps){
              steps[step] = {};
              steps[step].step_number = $scope.detail.steps[step].step_number;
              steps[step].step_title = $scope.detail.steps[step].step_title;
              steps[step].step_body = $scope.detail.steps[step].step_body;
              steps[step].duration = $scope.detail.steps[step].duration;
              steps[step].water_amount = $scope.detail.steps[step].water_amount;

              $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ newRecipeId +"/steps/", steps[step])//end step post
            };//end loop to clone steps
          })
          .then(function(){
            $location.path("/"+ username +"/clone/"+ newRecipeId);
          })//end post new recipe
        }; //end recipe clone function

        $scope.showEditNote = function(noteId) {
          noteView = "div.note-view" + noteId.toString();
          editNote = "article.edit-note" + noteId.toString();
          $(noteView).addClass("hidden")
          $(editNote).removeClass("hidden")
        }
          // $scope.showEditStep = function(stepId){
          //   stepId = "div." + stepId.toString();
          //   $(stepId).removeClass("hidden")
          // }

        $scope.editNote = function(note){
          var note_id = note.id
          $http.put("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/brewnotes/" + note_id + "/", note)
          .then( function () {
            $(editNote).addClass("hidden");
            $(noteView).removeClass("hidden");
          })
        } //end editNote function

        $scope.deleteNote = function(noteId) {
          var noteId = noteId
          if (window.confirm("Are you sure you want to delete this note?")){
            $http.delete("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/brewnotes/" + noteId + "/")
            .then(function(){var id = $scope.id;
            $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
              .then(function(response){
                $rootScope.notes = response.data.brewnotes;
              })
            })
          };
        }; //end deleteNote function

        $scope.showAddBrewNote = function(){
          $(".brew-form").toggleClass("hidden");
        };

        $scope.addBrewNote=function(){
          $http.post('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/brewnotes/', $scope.brewnote)
          .success(function (data) {
            $(".brew-form").toggleClass("hidden");
            var id = $scope.id;
            $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
              .then(function(response){
                $rootScope.notes = response.data.brewnotes;
              })
          })
        $scope.brewnote = { };
        $scope.addNote = false;
        }//Add Brew Note Form

        $scope.showNoteIcons = function(noteId){
          $(".note-icons").filter($("."+ noteId)).toggleClass("hidden");
        }

        $scope.makePublic = function(){ //makes recipes public
          if (!window.confirm("Are you sure you want to clone "+ $scope.detail.title +" ?")){
            return;
          };
          console.log("making it public!!!")
      }; // end makePublic function

      }) //end recipDetail controller

})();//END Angular IFEE
<commit_msg>add code to build recipe and steps in makePublic()<commit_after>;(function(){//IFEE

angular.module('brewKeeper')
      .controller('recipeDetail', function($scope, $http, $location, $routeParams, $rootScope){
          var id = $routeParams.id;
          var username = $routeParams.username;
          $scope.username = $routeParams.username;
          $scope.id = $routeParams.id;
          $(document).scrollTop(0);

          $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
            .then(function(response){
              $rootScope.detail = response.data;
              $rootScope.steps = response.data.steps;
              $rootScope.notes = response.data.brewnotes;
              var currentRating = $rootScope.detail.rating;
              $scope.rating = 0;
              $scope.ratings = [{
                  current: currentRating,
                  max: 5
              }];

          }) //end http.get

          $scope.rateRecipe = function (rating) {
            var newRating = {"rating": rating}
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/", newRating)
          }

          $scope.EliminateRecipe = function() {
            if (window.confirm("Are you sure you want to delete " + $scope.detail.title + "?")){
              $http.delete('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/').then(function(){
                $location.path('/'+ username);
              })
            };
          }; //end Eliminate function

          $scope.deleteStep = function(stepNumber, stepId){
            if (window.confirm("Are you sure you want to delete step " + stepNumber + "?")){
              $http.delete("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ stepId + "/").then(function(){
                $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id +'/')
                  .then(function(response){
                    $scope.steps = response.data.steps;
                    if(response.data.steps.length == 0){
                      $(".brew-it-button").addClass("hidden");
                      $(".no-steps").removeClass("hidden");
                    }
                  })
              })
            }
          } //end deleteStep function

          // $scope.showSteps = function(stepId){
          //   stepId= "div." + stepId.toString()
          //   $(stepId).removeClass("hidden")
          // };

          $scope.hideEditStep = function(stepId){
            stepId = "div." + stepId.toString();
            $(stepId).toggleClass("hidden")
          }

          $scope.editStep = function(step){
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step)
              .then(function() {
                $scope.hideEditStep(step.id)
                // $scope.showEditSteps(step.id)
              });
          } //end editStep function

          $scope.showEditStep = function(stepId){
            stepId = "div." + stepId.toString();
            $(stepId).toggleClass("hidden");
          }

          $scope.increaseStep = function(step){
            if(step.step_number >= $rootScope.steps.length){
              return
            }
            step.step_number++
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $rootScope.steps = response.data.steps;
                })
            })
          } //end increaseStep function

          $scope.decreaseStep = function(step){
            if(step.step_number <= 1){
              return
            }
            step.step_number--
            $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/"+ step.id + "/", step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $rootScope.steps = response.data.steps;
                })
            })
          } //end decreaseStep function

          $scope.step = { }//Might need to prepopulate this with empty strings for each key... Maybe...
          $scope.addStep=function(){ //add step function
            $scope.step.step_number = $scope.steps.length + 1;
            $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ id +"/steps/", $scope.step).then(function(){
              $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/')
                .then(function(response){
                  $(".brew-it-button").removeClass("hidden");
                  $(".no-steps").addClass("hidden");
                  $rootScope.steps = response.data.steps;
                })
            })
            $scope.step= { };
          } //end new step submit function

        $scope.showAddSteps = function(){
          $("form.create-new-step").toggleClass("hidden");
        };//Reveal "Add Step" when new recipe form is submitted.
        // $scope.hideAddSteps = function(){
        //   $("form.create-new-step").addClass("hidden");
        //   $("button.done-adding").addClass("hidden");
        // }; //hides the add step form

        $('.edit-button').on('click', function(){
          $('.edit-recipe').removeClass("hidden");
          $('.recipe-view').addClass("hidden");
        });

        $(".no-steps").click(function(){
          $scope.showAddSteps()
        })

      $scope.editRecipe = function(recipe){
        $http.patch("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/", recipe)
        .then( function () {
          $('.edit-recipe').addClass("hidden");
          $('.recipe-view').removeClass("hidden");
        })
      } //end editRecipe function
        $('.cancel-recipe-edit').on('click', function(){
          $('.recipe-view').removeClass("hidden");
          $('.edit-recipe').addClass("hidden");
        });

        // Function for cloning recipes
        $scope.cloneRecipe = function(){
          if (!window.confirm("Are you sure you want to clone "+ $scope.detail.title +" ?")){
            return;
          };
          var cloneData = {}
          cloneData.title = "Clone of: " + $scope.detail.title;
          cloneData.bean_name = $scope.detail.bean_name;
          cloneData.roast = $scope.detail.roast;
          cloneData.orientation = $scope.detail.orientation;
          cloneData.general_recipe_comment = $scope.detail.general_recipe_comment;
          cloneData.grind = $scope.detail.grind;
          cloneData.total_bean_amount = $scope.detail.total_bean_amount;
          cloneData.bean_units = $scope.detail.bean_units;
          cloneData.water_type = $scope.detail.water_type;
          cloneData.total_water_amount = $scope.detail.total_water_amount;
          cloneData.water_units = $scope.detail.water_units;
          cloneData.temp = $scope.detail.temp;
          cloneData.steps = [];

          $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/", cloneData).success(function(response){

            newRecipeId = response.id;
            steps = [];
            for(step in $scope.detail.steps){
              steps[step] = {};
              steps[step].step_number = $scope.detail.steps[step].step_number;
              steps[step].step_title = $scope.detail.steps[step].step_title;
              steps[step].step_body = $scope.detail.steps[step].step_body;
              steps[step].duration = $scope.detail.steps[step].duration;
              steps[step].water_amount = $scope.detail.steps[step].water_amount;

              $http.post("https://brew-keeper-api.herokuapp.com/api/users/"+ username +"/recipes/"+ newRecipeId +"/steps/", steps[step])//end step post
            };//end loop to clone steps
          })
          .then(function(){
            $location.path("/"+ username +"/clone/"+ newRecipeId);
          })//end post new recipe
        }; //end recipe clone function

        $scope.showEditNote = function(noteId) {
          noteView = "div.note-view" + noteId.toString();
          editNote = "article.edit-note" + noteId.toString();
          $(noteView).addClass("hidden")
          $(editNote).removeClass("hidden")
        }
          // $scope.showEditStep = function(stepId){
          //   stepId = "div." + stepId.toString();
          //   $(stepId).removeClass("hidden")
          // }

        $scope.editNote = function(note){
          var note_id = note.id
          $http.put("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/brewnotes/" + note_id + "/", note)
          .then( function () {
            $(editNote).addClass("hidden");
            $(noteView).removeClass("hidden");
          })
        } //end editNote function

        $scope.deleteNote = function(noteId) {
          var noteId = noteId
          if (window.confirm("Are you sure you want to delete this note?")){
            $http.delete("https://brew-keeper-api.herokuapp.com/api/users/"+ username + "/recipes/"+ id + "/brewnotes/" + noteId + "/")
            .then(function(){var id = $scope.id;
            $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
              .then(function(response){
                $rootScope.notes = response.data.brewnotes;
              })
            })
          };
        }; //end deleteNote function

        $scope.showAddBrewNote = function(){
          $(".brew-form").toggleClass("hidden");
        };

        $scope.addBrewNote=function(){
          $http.post('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + '/brewnotes/', $scope.brewnote)
          .success(function (data) {
            $(".brew-form").toggleClass("hidden");
            var id = $scope.id;
            $http.get('https://brew-keeper-api.herokuapp.com/api/users/' + username + '/recipes/' + id + "/")
              .then(function(response){
                $rootScope.notes = response.data.brewnotes;
              })
          })
        $scope.brewnote = { };
        $scope.addNote = false;
        }//Add Brew Note Form

        $scope.showNoteIcons = function(noteId){
          $(".note-icons").filter($("."+ noteId)).toggleClass("hidden");
        }

        $scope.makePublic = function(){ //makes recipes public
          if (!window.confirm("Are you sure you want to clone "+ $scope.detail.title +" ?")){
            return;
          };
          console.log("making it public!!!")
          var publicData = {}  //build the recipe
          publicData.title = "Public Copy of: " + $scope.detail.title;
          publicData.bean_name = $scope.detail.bean_name;
          publicData.roast = $scope.detail.roast;
          publicData.orientation = $scope.detail.orientation;
          publicData.general_recipe_comment = $scope.detail.general_recipe_comment;
          publicData.grind = $scope.detail.grind;
          publicData.total_bean_amount = $scope.detail.total_bean_amount;
          publicData.bean_units = $scope.detail.bean_units;
          publicData.water_type = $scope.detail.water_type;
          publicData.total_water_amount = $scope.detail.total_water_amount;
          publicData.water_units = $scope.detail.water_units;
          publicData.temp = $scope.detail.temp;
          publicData.steps = [];
          console.log(publicDatas)

          steps = []; //build the steps
          for(step in $scope.detail.steps){
            steps[step] = {};
            steps[step].step_number = $scope.detail.steps[step].step_number;
            steps[step].step_title = $scope.detail.steps[step].step_title;
            steps[step].step_body = $scope.detail.steps[step].step_body;
            steps[step].duration = $scope.detail.steps[step].duration;
            steps[step].water_amount = $scope.detail.steps[step].water_amount;
          }

          console.log(steps);
      }; // end makePublic function

      }) //end recipDetail controller

})();//END Angular IFEE
<|endoftext|>


<commit_before>import Component from '@ember/component';
import { computed } from '@ember/object';
import { or } from '@ember/object/computed';

export default Component.extend({
  tagName: '',

  nameOrLogin: computed('user.name', 'user.login', function () {
    let name = this.get('user.name');
    let login = this.get('user.login');
    return name || login;
  }),

  showUser: computed('nameOrLogin', 'eventType', function () {
    let nameOrLogin = this.nameOrLogin;
    let eventType = this.eventType;
    return nameOrLogin && eventType !== 'cron';
  }),

  userProvider: or('user.provider', 'provider')
});
<commit_msg>Do not show bot user<commit_after>import Component from '@ember/component';
import { computed } from '@ember/object';
import { or } from '@ember/object/computed';

export default Component.extend({
  tagName: '',

  nameOrLogin: computed('user.name', 'user.login', function () {
    let name = this.get('user.name');
    let login = this.get('user.login');
    return name || login;
  }),

  isBot: computed('user.vcs_id', function () {
    return this.get('user.vcsId') == '_bot_user';
  }),

  showUser: computed('nameOrLogin', 'eventType', 'isBot', function () {
    let nameOrLogin = this.nameOrLogin;
    let eventType = this.eventType;
    let isBot = this.isBot;
    return nameOrLogin && eventType !== 'cron' && !isBot;
  }),

  userProvider: or('user.provider', 'provider')
});
<|endoftext|>


<commit_before>var React = require('react');

var Dropzone = React.createClass({
  getInitialState: function() {
    return {
      isDragActive: false
    }
  },

  propTypes: {
    onDrop: React.PropTypes.func.isRequired,
    size: React.PropTypes.number,
    style: React.PropTypes.object
  },

  onDragLeave: function(e) {
    this.setState({
      isDragActive: false
    });
  },

  onDragOver: function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = "copy";

    this.setState({
      isDragActive: true
    });
  },

  onDrop: function(e) {
    e.preventDefault();

    this.setState({
      isDragActive: false
    });

    var files;
    if (e.dataTransfer) {
      files = e.dataTransfer.files;
    } else if (e.target) {
      files = e.target.files;
    }

    for (var i = 0; i < files.length; i++) {
      files[i].preview = URL.createObjectURL(files[i]);
    }

    if (this.props.onDrop) {
      files = Array.prototype.slice.call(files);
      this.props.onDrop(files);
    }
  },

  onClick: function () {
    this.refs.fileInput.getDOMNode().click();
  },

  render: function() {

    var className = this.props.className || 'dropzone';
    if (this.state.isDragActive) {
      className += ' active';
    };

    var style = this.props.style || {
      width: this.props.size || 100,
      height: this.props.size || 100,
      borderStyle: this.state.isDragActive ? "solid" : "dashed"
    };

    return (
        React.createElement("div", {className: className, style: style, onClick: this.onClick, onDragLeave: this.onDragLeave, onDragOver: this.onDragOver, onDrop: this.onDrop},
            React.createElement("input", {style: {display: 'none'}, type: "file", multiple: true, ref: "fileInput", onChange: this.onDrop}),
            this.props.children
        )
    );
  }

});

module.exports = Dropzone;
<commit_msg>Add `supportClick` prop and make default true.<commit_after>var React = require('react');

var Dropzone = React.createClass({
  getDefaultProps: function() {
    return {
      supportClick: true
    };
  },

  getInitialState: function() {
    return {
      isDragActive: false
    }
  },

  propTypes: {
    onDrop: React.PropTypes.func.isRequired,
    size: React.PropTypes.number,
    style: React.PropTypes.object,
    supportClick: React.PropTypes.bool
  },

  onDragLeave: function(e) {
    this.setState({
      isDragActive: false
    });
  },

  onDragOver: function(e) {
    e.preventDefault();
    e.dataTransfer.dropEffect = "copy";

    this.setState({
      isDragActive: true
    });
  },

  onDrop: function(e) {
    e.preventDefault();

    this.setState({
      isDragActive: false
    });

    var files;
    if (e.dataTransfer) {
      files = e.dataTransfer.files;
    } else if (e.target) {
      files = e.target.files;
    }

    for (var i = 0; i < files.length; i++) {
      files[i].preview = URL.createObjectURL(files[i]);
    }

    if (this.props.onDrop) {
      files = Array.prototype.slice.call(files);
      this.props.onDrop(files);
    }
  },

  onClick: function () {
    if (this.props.supportClick === true) {
      this.refs.fileInput.getDOMNode().click();
    }
  },

  render: function() {

    var className = this.props.className || 'dropzone';
    if (this.state.isDragActive) {
      className += ' active';
    };

    var style = this.props.style || {
      width: this.props.size || 100,
      height: this.props.size || 100,
      borderStyle: this.state.isDragActive ? "solid" : "dashed"
    };

    return (
        React.createElement("div", {className: className, style: style, onClick: this.onClick, onDragLeave: this.onDragLeave, onDragOver: this.onDragOver, onDrop: this.onDrop},
            React.createElement("input", {style: {display: 'none'}, type: "file", multiple: true, ref: "fileInput", onChange: this.onDrop}),
            this.props.children
        )
    );
  }

});

module.exports = Dropzone;
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var Q = require('q');
var utils = require('./utils');
var Job = require('./Job');

/**
 * Test runner.
 *
 * @constructor
 * @param {Object} properties - Configuration options.
 * @param {String} framework - The unit test framework's name. Can be 'yasmine', 'qunit',
 *   'YUI Test', 'mocha' or 'custom'.
 * @param {Function} onProgress - Progress handler.
 */
var TestRunner = function (properties, framework, onProgress) {
  this.user = properties.username;
  this.key = properties.key;
  this.pollInterval = properties.pollInterval;
  this.framework = framework;
  this.tunneled = properties.tunneled;
  this.tunnelId = properties.identifier;
  this.testName = properties.testname;
  this.build = properties.build;
  this.sauceConfig = properties.sauceConfig;
  this.onTestComplete = properties.onTestComplete;
  this.throttled = properties.throttled;
  this.browsers = properties.browsers;
  this.urls = properties.url || properties.urls;
  this.maxRetries = properties.maxRetries;
  this.onProgress = onProgress;

  if (properties['max-duration']) {
    // max-duration is actually a sauce selenium capability
    this.sauceConfig['max-duration'] = properties['max-duration'];
  }
  this.urls = this.urls.length !== undefined ? this.urls : [this.urls];
  this.numberOfJobs = this.browsers.length * this.urls.length;
  this.startedJobs = 0;
};

/**
* Reports progress.
* @param {Object} progress - Progress data.
*/
TestRunner.prototype.reportProgress = function (progress) {
  if (this.onProgress) {
    this.onProgress(progress);
  }
};

/**
 * Runs the test in all of the browsers-URL combinations.
 *
 * @returns {Object} - A promise which will be eventually resolved with the test results
 *   (a boolean). Progress is reported after each job is started and completed.
 */
TestRunner.prototype.runTests = function () {
  var me = this;
  var throttledRunTest, promises;

  throttledRunTest = utils.limitConcurrency(this.runTest.bind(this), this.throttled || Number.MAX_VALUE);

  promises = this.urls
    .map(function (url) {
      return this.browsers.map(function (browser) {
        return throttledRunTest(browser, url);
      });
    }, this)
    .reduce(function (acc, promisesForUrl) {
      return acc.concat(promisesForUrl);
    }, []);

  return Q
    .all(promises)
    .then(function (results) {
      var passed = results.indexOf(false) === -1;

      me.reportProgress({
        type: 'testCompleted',
        passed: passed
      });

      return passed;
    });
};

/**
 * Runs a test with the specified URL in the specified environment.
 *
 * @param {Object} browser - The environment to run the test on.
 * @param {String} url - An URL that will be loaded in the browsers.
 * @returns {Object} - A promise which will be eventually resolved with the test results
 *   (a boolean). Progress is reported after the job is started and completed.
 */
TestRunner.prototype.runTest = function (browser, url) {
  var me = this;
  var retry = 0;

  function getResult() {
    var job = new Job(me.user, me.key, me.framework, me.pollInterval, url, browser,
      me.build, me.testName, me.sauceConfig, me.tunneled, me.tunnelId);

    return job
      .start()
      .then(function () { return job.getResult(); })
      .then(function (result) {
        // when a test times out then the result property is a string
        if (result.result &&
          result.result.indexOf &&
          result.result.indexOf('Test exceeded maximum duration') > -1 &&
          retry < me.maxRetries) {
          retry += 1;

          me.reportProgress({
            type: 'retrying'
          });

          return job
            .stop()
          // delete the timed out job otherwise the SauceLabs badge/status image would
          // indicate failure
            .then(function () { return job.del(); })
            .then(getResult);
        }
        return result;
      });
  }

  return Q
    .fcall(function () {
      me.startedJobs += 1;
      me.reportProgress({
        type: 'jobStarted',
        numberOfJobs: me.numberOfJobs,
        startedJobs: me.startedJobs
      });
    })
    .then(getResult)
    .then(function (result) {
      if (me.onTestComplete) {
        var clone = _.clone(result, true);
        return Q
          .nfcall(me.onTestComplete, clone)
          .then(function (passed) {
            if (passed !== undefined) {
              result.passed = !!passed;
            }
            return result;
          });
      }
      return result;
    })
    .then(function (result) {
      me.reportProgress({
        type: 'jobCompleted',
        url: url,
        jobUrl: result.url,
        platform: result.platform,
        passed: result.passed,
        tunnelId: me.tunnelId
      });

      return result.passed;
    });
};

module.exports = TestRunner;<commit_msg>If the DELETE command throws when the retry logic tries to delete the failed job, the exception is swallowed.<commit_after>'use strict';

var _ = require('lodash');
var Q = require('q');
var utils = require('./utils');
var Job = require('./Job');

/**
 * Test runner.
 *
 * @constructor
 * @param {Object} properties - Configuration options.
 * @param {String} framework - The unit test framework's name. Can be 'yasmine', 'qunit',
 *   'YUI Test', 'mocha' or 'custom'.
 * @param {Function} onProgress - Progress handler.
 */
var TestRunner = function (properties, framework, onProgress) {
  this.user = properties.username;
  this.key = properties.key;
  this.pollInterval = properties.pollInterval;
  this.framework = framework;
  this.tunneled = properties.tunneled;
  this.tunnelId = properties.identifier;
  this.testName = properties.testname;
  this.build = properties.build;
  this.sauceConfig = properties.sauceConfig;
  this.onTestComplete = properties.onTestComplete;
  this.throttled = properties.throttled;
  this.browsers = properties.browsers;
  this.urls = properties.url || properties.urls;
  this.maxRetries = properties.maxRetries;
  this.onProgress = onProgress;

  if (properties['max-duration']) {
    // max-duration is actually a sauce selenium capability
    this.sauceConfig['max-duration'] = properties['max-duration'];
  }
  this.urls = this.urls.length !== undefined ? this.urls : [this.urls];
  this.numberOfJobs = this.browsers.length * this.urls.length;
  this.startedJobs = 0;
};

/**
* Reports progress.
* @param {Object} progress - Progress data.
*/
TestRunner.prototype.reportProgress = function (progress) {
  if (this.onProgress) {
    this.onProgress(progress);
  }
};

/**
 * Runs the test in all of the browsers-URL combinations.
 *
 * @returns {Object} - A promise which will be eventually resolved with the test results
 *   (a boolean). Progress is reported after each job is started and completed.
 */
TestRunner.prototype.runTests = function () {
  var me = this;
  var throttledRunTest, promises;

  throttledRunTest = utils.limitConcurrency(this.runTest.bind(this), this.throttled || Number.MAX_VALUE);

  promises = this.urls
    .map(function (url) {
      return this.browsers.map(function (browser) {
        return throttledRunTest(browser, url);
      });
    }, this)
    .reduce(function (acc, promisesForUrl) {
      return acc.concat(promisesForUrl);
    }, []);

  return Q
    .all(promises)
    .then(function (results) {
      var passed = results.indexOf(false) === -1;

      me.reportProgress({
        type: 'testCompleted',
        passed: passed
      });

      return passed;
    });
};

/**
 * Runs a test with the specified URL in the specified environment.
 *
 * @param {Object} browser - The environment to run the test on.
 * @param {String} url - An URL that will be loaded in the browsers.
 * @returns {Object} - A promise which will be eventually resolved with the test results
 *   (a boolean). Progress is reported after the job is started and completed.
 */
TestRunner.prototype.runTest = function (browser, url) {
  var me = this;
  var retry = 0;

  function getResult() {
    var job = new Job(me.user, me.key, me.framework, me.pollInterval, url, browser,
      me.build, me.testName, me.sauceConfig, me.tunneled, me.tunnelId);

    return job
      .start()
      .then(function () { return job.getResult(); })
      .then(function (result) {
        // when a test times out then the result property is a string
        if (result.result &&
          result.result.indexOf &&
          result.result.indexOf('Test exceeded maximum duration') > -1 &&
          retry < me.maxRetries) {
          retry += 1;

          me.reportProgress({
            type: 'retrying'
          });

          return job
            .stop()
          // Try to delete the timed out job in order to prevent the SauceLabs badge to
          // indicate failure. The del() function can also fail, in that case swallow the
          // exception.
            .then(function () { return job.del().fail(_.noop); })
            .then(getResult);
        }
        return result;
      });
  }

  return Q
    .fcall(function () {
      me.startedJobs += 1;
      me.reportProgress({
        type: 'jobStarted',
        numberOfJobs: me.numberOfJobs,
        startedJobs: me.startedJobs
      });
    })
    .then(getResult)
    .then(function (result) {
      if (me.onTestComplete) {
        var clone = _.clone(result, true);
        return Q
          .nfcall(me.onTestComplete, clone)
          .then(function (passed) {
            if (passed !== undefined) {
              result.passed = !!passed;
            }
            return result;
          });
      }
      return result;
    })
    .then(function (result) {
      me.reportProgress({
        type: 'jobCompleted',
        url: url,
        jobUrl: result.url,
        platform: result.platform,
        passed: result.passed,
        tunnelId: me.tunnelId
      });

      return result.passed;
    });
};

module.exports = TestRunner;
<|endoftext|>


<commit_before>import styled from 'styled-components'
import media from 'utils/mediaqueries'
import muiThemeable from 'material-ui/styles/muiThemeable'

const View = styled.div`
  background-color: ${(props) => props.muiTheme.palette.bodyColor};
  padding: 4rem 1rem;
  padding-left: ${(props) => (props.left ? '0rem' : '0rem')};
  padding-right: ${(props) => (props.right ? '0rem' : '0rem')};
  padding-top: ${(props) => (props.top ? `${props.top}rem` : '0rem')};
  padding-bottom: ${(props) => (props.bottom ? `${props.bottom}rem` : '2rem')};

  ${media.sm} {
    padding-left: ${(props) => (props.left ? '2rem' : '0rem')};
    padding-right: ${(props) => (props.right ? '2rem' : '0rem')};
    padding-top: ${(props) => (props.top ? `${props.top}rem` : '4rem')};
    padding-bottom: ${(props) =>
      props.bottom ? `${props.bottom}rem` : '4rem'};
  }

  ${media.lg} {
    padding-left: ${(props) => (props.left ? '4rem' : '0rem')};
    padding-right: ${(props) => (props.right ? '4rem' : '0rem')};
    padding-top: ${(props) => (props.top ? `${props.top}rem` : '4rem')};
    padding-bottom: ${(props) =>
      props.bottom ? `${props.bottom}rem` : '4rem'};
  }
`

export default muiThemeable()(View)
<commit_msg>remove extra padding on top<commit_after>import styled from 'styled-components'
import media from 'utils/mediaqueries'
import muiThemeable from 'material-ui/styles/muiThemeable'

const View = styled.div`
  background-color: ${(props) => props.muiTheme.palette.bodyColor};
  padding-left: ${(props) => (props.left ? '0rem' : '0rem')};
  padding-right: ${(props) => (props.right ? '0rem' : '0rem')};
  padding-top: ${(props) => (props.top ? `${props.top}rem` : '0rem')};
  padding-bottom: ${(props) => (props.bottom ? `${props.bottom}rem` : '2rem')};

  ${media.sm} {
    padding-left: ${(props) => (props.left ? '2rem' : '0rem')};
    padding-right: ${(props) => (props.right ? '2rem' : '0rem')};
    padding-top: ${(props) => (props.top ? `${props.top}rem` : '4rem')};
    padding-bottom: ${(props) =>
    props.bottom ? `${props.bottom}rem` : '4rem'};
  }

  ${media.lg} {
    padding-left: ${(props) => (props.left ? '4rem' : '0rem')};
    padding-right: ${(props) => (props.right ? '4rem' : '0rem')};
    padding-top: ${(props) => (props.top ? `${props.top}rem` : '3rem')};
    padding-bottom: ${(props) =>
    props.bottom ? `${props.bottom}rem` : '4rem'};
  }
`

export default muiThemeable()(View)
<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter;
var util         = require('util');
var WebSocket    = require('ws');
var utils        = require('./utils');

/**
 * Server
 *
 * Options must contain `url` to
 * WebSocket server
 *
 * @constructor
 * @param {Object} remote
 * @param {Object} opts
 */

function Server(remote, opts) {
  EventEmitter.call(this);

  if (typeof opts !== 'object' || typeof opts.url !== 'string') {
    throw new Error('Invalid server configuration.');
  }

  var self = this;

  this._remote         = remote;
  this._opts           = opts;

  this._ws             = void(0);
  this._connected      = false;
  this._should_connect = false;
  this._state          = void(0);

  this._id             = 0;
  this._retry          = 0;

  this._requests       = { };

  this.on('message', function(message) {
    self._handle_message(message);
  });

  this.on('response_subscribe', function(message) {
    self._handle_response_subscribe(message);
  });
};

util.inherits(Server, EventEmitter);

/**
 * Server states that we will treat as the server being online.
 *
 * Our requirements are that the server can process transactions and notify
 * us of changes.
 */

Server.online_states = [
    'syncing'
  , 'tracking'
  , 'proposing'
  , 'validating'
  , 'full'
];

/**
 * Determine if a server status qualifies
 * as 'online'
 *
 * @param {String} status
 * @return {Boolean}
 * @api private
 */

Server.prototype.is_online = function(status) {
  return Server.online_states.indexOf(status) !== -1;
};

/**
 * Connect to WebSocket server
 */

Server.prototype.connect = function() {
  var self = this;

  // We don't connect if we believe we're already connected. This means we have
  // recently received a message from the server and the WebSocket has not
  // reported any issues either. If we do fail to ping or the connection drops,
  // we will automatically reconnect.
  if (this._connected === true) return;

  if (this._remote.trace) {
    console.log('server: connect: %s', this._opts.url);
  }

  // Ensure any existing socket is given the command to close first.
  if (this._ws) {
    this._ws.close();
  }

  var ws = this._ws = new WebSocket(this._opts.url);

  this._should_connect = true;

  self.emit('connecting');

  ws.onopen = function() {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    self.emit('socket_open');

    // Subscribe to events
    var request = self._remote._server_prepare_subscribe();
    self.request(request);
  };

  ws.onerror = function(e) {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    if (self._remote.trace) {
      console.log('server: onerror: %s', e.data || e);
    }

    // Most connection errors for WebSockets are conveyed as 'close' events with
    // code 1006. This is done for security purposes and therefore unlikely to
    // ever change.

    // This means that this handler is hardly ever called in practice. If it is,
    // it probably means the server's WebSocket implementation is corrupt, or
    // the connection is somehow producing corrupt data.

    // Most WebSocket applications simply log and ignore this error. Once we
    // support for multiple servers, we may consider doing something like
    // lowering this server's quality score.

    // However, in Node.js this event may be triggered instead of the close
    // event, so we need to handle it.
    handleConnectionClose();
  };

  // Failure to open.
  ws.onclose = function() {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    if (self._remote.trace) {
      console.log('server: onclose: %s', ws.readyState);
    }

    handleConnectionClose();
  };

  function handleConnectionClose() {
    self.emit('socket_close');
    self._set_state('offline');

    // Prevent additional events from this socket
    ws.removeAllListeners();
    ws.on('error', function() {});

    // Should we be connected?
    if (!self._should_connect) return;

    // Delay and retry.
    self._retry += 1;

    self._retry_timer = setTimeout(function retryTimeout() {
      if (self._remote.trace) console.log('server: retry');
      if (!self._should_connect) return;
      self.connect();
    }, self._retry < 40
        ? 1000/20           // First, for 2 seconds: 20 times per second
        : self._retry < 40+60
          ? 1000            // Then, for 1 minute: once per second
          : self._retry < 40+60+60
            ? 10*1000       // Then, for 10 minutes: once every 10 seconds
            : 30*1000);     // Then: once every 30 seconds
  };

  ws.onmessage = function(msg) {
    self.emit('message', msg.data);
  };
};

/**
 * Disconnect from WebSocket server
 *
 * @api public
 */

Server.prototype.disconnect = function() {
  this._should_connect = false;
  this._set_state('offline');
  if (this._ws) {
    this._ws.close();
  }
};

/**
 * Send stringified message to WebSocket server
 *
 * @param {Object} message
 * @api private
 */

Server.prototype.send = function(message) {
  if (this._ws) {
    this._ws.send(JSON.stringify(message));
  }
};

/**
 * Submit a Request object to this server.
 *
 * @param {Object} request
 * @api public
 */

Server.prototype.request = function(request) {
  var self  = this;

  // Only bother if we are still connected.
  if (self._ws) {
    request.message.id = self._id;

    self._requests[request.message.id] = request;

    // Advance message ID
    self._id++;

    if (self._connected || (request.message.command === 'subscribe'
                            && self._ws.readyState === 1)) {
      if (self._remote.trace) {
        utils.logObject('server: request: %s', request.message);
      }
      self.send(request.message);
    } else {
      // XXX There are many ways to make self smarter.
      self.once('connect', function () {
        if (self._remote.trace) {
          utils.logObject('server: request: %s', request.message);
        }
        self.send(request.message);
      });
    }
  } else {
    if (self._remote.trace) {
      utils.logObject('server: request: DROPPING: %s', request.message);
    }
  }
};

/**
 * Set server state
 *
 * Examples:
 *
 *  set_state('online')
 *  set_state('offline')
 *
 * @param {String} state
 * @api private
 */

Server.prototype._set_state = function(state) {
  if (state !== this._state) {
    this._state = state;

    this.emit('state', state);

    if (state === 'online') {
      this._connected = true;
      this.emit('connect');
    } else if (state === 'offline') {
      this._connected = false;
      this.emit('disconnect');
    }
  }
};

/**
 * Handle WebSocket message
 *
 * @param {String} json
 * @api private
 */

Server.prototype._handle_message = function(json) {
  var self       = this;
  var unexpected = false;
  var message;

  try { message = JSON.parse(json); } catch(exception) { }

  var unexpected = typeof message !== 'object' || typeof message.type !== 'string';

  if (unexpected) {
    // We received a malformed response from the server
  }

  if (!unexpected) {
    switch (message.type) {
      case 'response':
        // A response to a request.
        var request = self._requests[message.id];

        delete self._requests[message.id];

        if (!request) {
          if (self._remote.trace) {
            utils.logObject('server: UNEXPECTED: %s', message);
          }
        } else if (message.status === 'success') {
          if (self._remote.trace) {
            utils.logObject('server: response: %s', message);
          }

          request.emit('success', message.result);

          [ self, self._remote ].forEach(function(emitter) {
            emitter.emit('response_' + request.message.command, message.result, request, message);
          });
        } else if (message.error) {
          if (self._remote.trace) {
            utils.logObject('server: error: %s', message);
          }

          request.emit('error', {
            'error'         : 'remoteError',
            'error_message' : 'Remote reported an error.',
            'remote'        : message
          });
        }
        break;

      case 'serverStatus':
        // This message is only received when online.
        // As we are connected, it is the definitive final state.
        self._set_state(self.is_online(message.server_status) ? 'online' : 'offline');
        break;
    }
  }
};

/**
 * Handle subscribe response
 *
 * @param {Object} message
 * @api private
 */

Server.prototype._handle_response_subscribe = function(message) {
  var self = this;

  self._server_status = message.server_status;

  if (self.is_online(message.server_status)) {
    self._set_state('online');
  }
};

exports.Server = Server;

// vim:sw=2:sts=2:ts=8:et
<commit_msg>Revert "Add JSDoc style comments"<commit_after>var EventEmitter = require('events').EventEmitter;
var util         = require('util');
var WebSocket    = require('ws');
var utils        = require('./utils');

/**
 * Server
 *
 * @constructor
 * @param {Object} remote
 * @param {Object} opts
 */

function Server(remote, opts) {
  EventEmitter.call(this);

  if (typeof opts !== 'object' || typeof opts.url !== 'string') {
    throw new Error('Invalid server configuration.');
  }

  var self = this;

  this._remote         = remote;
  this._opts           = opts;

  this._ws             = void(0);
  this._connected      = false;
  this._should_connect = false;
  this._state          = void(0);

  this._id             = 0;
  this._retry          = 0;

  this._requests       = { };

  this.on('message', function(message) {
    self._handle_message(message);
  });

  this.on('response_subscribe', function(message) {
    self._handle_response_subscribe(message);
  });
};

util.inherits(Server, EventEmitter);

/**
 * Server states that we will treat as the server being online.
 *
 * Our requirements are that the server can process transactions and notify
 * us of changes.
 */
Server.online_states = [
    'syncing'
  , 'tracking'
  , 'proposing'
  , 'validating'
  , 'full'
];

Server.prototype.is_online = function(status) {
  return Server.online_states.indexOf(status) !== -1;
};

Server.prototype.connect = function() {
  var self = this;

  // We don't connect if we believe we're already connected. This means we have
  // recently received a message from the server and the WebSocket has not
  // reported any issues either. If we do fail to ping or the connection drops,
  // we will automatically reconnect.
  if (this._connected === true) return;

  if (this._remote.trace) {
    console.log('server: connect: %s', this._opts.url);
  }

  // Ensure any existing socket is given the command to close first.
  if (this._ws) {
    this._ws.close();
  }

  var ws = this._ws = new WebSocket(this._opts.url);

  this._should_connect = true;

  self.emit('connecting');

  ws.onopen = function() {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    self.emit('socket_open');

    // Subscribe to events
    var request = self._remote._server_prepare_subscribe();
    self.request(request);
  };

  ws.onerror = function(e) {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    if (self._remote.trace) {
      console.log('server: onerror: %s', e.data || e);
    }

    // Most connection errors for WebSockets are conveyed as 'close' events with
    // code 1006. This is done for security purposes and therefore unlikely to
    // ever change.

    // This means that this handler is hardly ever called in practice. If it is,
    // it probably means the server's WebSocket implementation is corrupt, or
    // the connection is somehow producing corrupt data.

    // Most WebSocket applications simply log and ignore this error. Once we
    // support for multiple servers, we may consider doing something like
    // lowering this server's quality score.

    // However, in Node.js this event may be triggered instead of the close
    // event, so we need to handle it.
    handleConnectionClose();
  };

  // Failure to open.
  ws.onclose = function() {
    // If we are no longer the active socket, simply ignore any event
    if (ws !== self._ws) return;

    if (self._remote.trace) {
      console.log('server: onclose: %s', ws.readyState);
    }

    handleConnectionClose();
  };

  function handleConnectionClose() {
    self.emit('socket_close');
    self._set_state('offline');

    // Prevent additional events from this socket
    ws.removeAllListeners();
    ws.on('error', function() {});

    // Should we be connected?
    if (!self._should_connect) return;

    // Delay and retry.
    self._retry += 1;

    self._retry_timer = setTimeout(function retryTimeout() {
      if (self._remote.trace) console.log('server: retry');
      if (!self._should_connect) return;
      self.connect();
    }, self._retry < 40
        ? 1000/20           // First, for 2 seconds: 20 times per second
        : self._retry < 40+60
          ? 1000            // Then, for 1 minute: once per second
          : self._retry < 40+60+60
            ? 10*1000       // Then, for 10 minutes: once every 10 seconds
            : 30*1000);     // Then: once every 30 seconds
  };

  ws.onmessage = function(msg) {
    self.emit('message', msg.data);
  };
};

Server.prototype.disconnect = function() {
  this._should_connect = false;
  this._set_state('offline');
  if (this._ws) {
    this._ws.close();
  }
};

Server.prototype.send = function(message) {
  if (this._ws) {
    this._ws.send(JSON.stringify(message));
  }
};

/**
 * Submit a Request object to this server.
 */
Server.prototype.request = function(request) {
  var self  = this;

  // Only bother if we are still connected.
  if (self._ws) {
    request.message.id = self._id;

    self._requests[request.message.id] = request;

    // Advance message ID
    self._id++;

    if (self._connected || (request.message.command === 'subscribe'
                            && self._ws.readyState === 1)) {
      if (self._remote.trace) {
        utils.logObject('server: request: %s', request.message);
      }
      self.send(request.message);
    } else {
      // XXX There are many ways to make self smarter.
      self.once('connect', function () {
        if (self._remote.trace) {
          utils.logObject('server: request: %s', request.message);
        }
        self.send(request.message);
      });
    }
  } else {
    if (self._remote.trace) {
      utils.logObject('server: request: DROPPING: %s', request.message);
    }
  }
};

Server.prototype._set_state = function(state) {
  if (state !== this._state) {
    this._state = state;

    this.emit('state', state);

    if (state === 'online') {
      this._connected = true;
      this.emit('connect');
    } else if (state === 'offline') {
      this._connected = false;
      this.emit('disconnect');
    }
  }
};

Server.prototype._handle_message = function(json) {
  var self       = this;
  var unexpected = false;
  var message;

  try { message = JSON.parse(json); } catch(exception) { }

  var unexpected = typeof message !== 'object' || typeof message.type !== 'string';

  if (unexpected) {
    // We received a malformed response from the server
  }

  if (!unexpected) {
    switch (message.type) {
      case 'response':
        // A response to a request.
        var request = self._requests[message.id];

        delete self._requests[message.id];

        if (!request) {
          if (self._remote.trace) {
            utils.logObject('server: UNEXPECTED: %s', message);
          }
        } else if (message.status === 'success') {
          if (self._remote.trace) {
            utils.logObject('server: response: %s', message);
          }

          request.emit('success', message.result);

          [ self, self._remote ].forEach(function(emitter) {
            emitter.emit('response_' + request.message.command, message.result, request, message);
          });
        } else if (message.error) {
          if (self._remote.trace) {
            utils.logObject('server: error: %s', message);
          }

          request.emit('error', {
            'error'         : 'remoteError',
            'error_message' : 'Remote reported an error.',
            'remote'        : message
          });
        }
        break;

      case 'serverStatus':
        // This message is only received when online.
        // As we are connected, it is the definitive final state.
        self._set_state(self.is_online(message.server_status) ? 'online' : 'offline');
        break;
    }
  }
};

Server.prototype._handle_response_subscribe = function(message) {
  var self = this;

  self._server_status = message.server_status;

  if (self.is_online(message.server_status)) {
    self._set_state('online');
  }
};

exports.Server = Server;

// vim:sw=2:sts=2:ts=8:et
<|endoftext|>


<commit_before>
let r = require('r-dom')
let mori = require('mori')
let PropTypes = require('react').PropTypes
let ShallowComponent = require('./shallow-component')

let Icon = require('./icon')

let AppActions = require('../actions/app-actions')

/**
 * Displays our current progress when checking for updates, etc.
 */
class StatusBar extends ShallowComponent {
  render () {
    let t = this.t
    let update = this.props.update
    let status = mori.get(update, 'status')
    let error = mori.get(update, 'error')
    let available = mori.get(update, 'available')
    let downloaded = mori.get(update, 'downloaded')
    let checking = mori.get(update, 'checking')
    let uptodate = mori.get(update, 'uptodate')

    let children = []
    let active = true

    let onClick = () => null

    if (status) {
      onClick = AppActions.dismiss_status
      children = [
        r(Icon, {icon: 'heart-filled'}),
        r.span(status),
        r(Icon, {icon: 'cross'})
      ]
    } else if (error) {
      onClick = AppActions.dismiss_status
      children = [
        r(Icon, {icon: 'heart-broken'}),
        r.span('Error while checking for update: ' + error),
        r(Icon, {icon: 'cross'})
      ]
    } else if (downloaded) {
      onClick = AppActions.apply_self_update
      children = [
        r(Icon, {icon: 'install'}),
        r.span(t('status.downloaded'))
      ]
    } else if (available) {
      children = [
        r(Icon, {icon: 'download'}),
        r.span(t('status.downloading'))
      ]
    } else if (checking) {
      children = [
        r(Icon, {icon: 'stopwatch'}),
        r.span(t('status.checking'))
      ]
    } else if (uptodate) {
      children = [
        r(Icon, {icon: 'like'}),
        r.span(t('status.uptodate'))
      ]
    } else {
      active = false
    }

    return (
      r.div({classSet: {status_bar: true, active}},
        r.div({className: 'message', onClick}, children)
      )
    )
  }
}

StatusBar.propTypes = {
  update: PropTypes.any
}

module.exports = StatusBar
<commit_msg>Closes #351<commit_after>
let r = require('r-dom')
let mori = require('mori')
let PropTypes = require('react').PropTypes
let ShallowComponent = require('./shallow-component')

let Icon = require('./icon')

let AppActions = require('../actions/app-actions')

/**
 * Displays our current progress when checking for updates, etc.
 */
class StatusBar extends ShallowComponent {
  render () {
    let t = this.t
    let state = this.props.state
    let update = mori.get(state, 'update')
    let status = mori.get(update, 'status')
    let error = mori.get(update, 'error')
    let available = mori.get(update, 'available')
    let downloaded = mori.get(update, 'downloaded')
    let checking = mori.get(update, 'checking')
    let uptodate = mori.get(update, 'uptodate')

    let children = []
    let active = true

    let onClick = () => null

    if (status) {
      onClick = AppActions.dismiss_status
      children = [
        r(Icon, {icon: 'heart-filled'}),
        r.span(status),
        r(Icon, {icon: 'cross'})
      ]
    } else if (error) {
      onClick = AppActions.dismiss_status
      children = [
        r(Icon, {icon: 'heart-broken'}),
        r.span('Error while checking for update: ' + error),
        r(Icon, {icon: 'cross'})
      ]
    } else if (downloaded) {
      onClick = AppActions.apply_self_update
      children = [
        r(Icon, {icon: 'install'}),
        r.span(t('status.downloaded'))
      ]
    } else if (available) {
      children = [
        r(Icon, {icon: 'download'}),
        r.span(t('status.downloading'))
      ]
    } else if (checking) {
      children = [
        r(Icon, {icon: 'stopwatch'}),
        r.span(t('status.checking'))
      ]
    } else if (uptodate) {
      children = [
        r(Icon, {icon: 'like'}),
        r.span(t('status.uptodate'))
      ]
    } else {
      active = false
    }

    return (
      r.div({classSet: {status_bar: true, active}},
        r.div({className: 'message', onClick}, children)
      )
    )
  }
}

StatusBar.propTypes = {
  update: PropTypes.any
}

module.exports = StatusBar
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2017, FREDRIK HOLMÉN. All rights reserved.
 */

 module.exports = require('./src/rootextract')
<commit_msg>Cleaning up<commit_after><|endoftext|>


<commit_before>import React from 'react';
import Relay from 'react-relay';
import DocumentTitle from 'react-document-title';
import { seconds } from 'metrick/duration';

import Button from '../shared/Button';
import FlashesStore from '../../stores/FlashesStore';
import FriendlyTime from "../shared/FriendlyTime";
import JobLink from '../shared/JobLink';
import PageWithContainer from '../shared/PageWithContainer';
import Panel from '../shared/Panel';
import Spinner from '../shared/Spinner';
import permissions from '../../lib/permissions';
import { getColourForConnectionState, getLabelForConnectionState } from './shared';

import AgentStopMutation from '../../mutations/AgentStop';

class AgentShow extends React.Component {
  static propTypes = {
    agent: React.PropTypes.shape({
      id: React.PropTypes.string,
      name: React.PropTypes.string.isRequired,
      connectionState: React.PropTypes.string,
      permissions: React.PropTypes.shape({
        agentStop: React.PropTypes.shape({
          allowed: React.PropTypes.bool
        })
      })
    }),
    relay: React.PropTypes.object.isRequired
  };

  state = {
    stopping: false
  };

  componentDidMount() {
    this._agentRefreshInterval = setInterval(this.fetchUpdatedData, 5::seconds);

    // Once the agent's show page has mounted in DOM, switch `isMounted` to
    // true which will trigger a load of all the additional information we need
    // to show this page.
    this.props.relay.setVariables({ isMounted: true });
  }

  componentWillUnmount() {
    clearInterval(this._agentRefreshInterval);
  }

  fetchUpdatedData = () => {
    this.props.relay.forceFetch(true);
  };

  renderExtraItem(title, content) {
    return (

        {title}: {content}

    );
  }

  renderExtras(agent) {
    const extras = [];

    if (agent.version) {
      extras.push(this.renderExtraItem('Version', agent.version));
    }

    if (agent.hostname) {
      extras.push(this.renderExtraItem('Hostname', agent.hostname));
    }

    if (agent.pid) {
      extras.push(this.renderExtraItem('PID', agent.pid));
    }

    if (agent.ipAddress) {
      extras.push(this.renderExtraItem('IP Address', agent.ipAddress));
    }

    if (agent.userAgent) {
      extras.push(this.renderExtraItem('User Agent', agent.userAgent));
    }

    if (agent.operatingSystem) {
      extras.push(this.renderExtraItem('OS', agent.operatingSystem));
    }

    if (agent.priority) {
      extras.push(this.renderExtraItem('Priority', agent.priority));
    }

    if (agent.job) {
      extras.push(this.renderExtraItem('Running', <JobLink job={agent.job} />));
    }

    if (agent.connectedAt) {
      extras.push(this.renderExtraItem(
        'Connected',

          <FriendlyTime value={agent.connectedAt} />
          {agent.pingedAt && agent.connectionState === 'connected' &&
             (last check-in was <FriendlyTime value={agent.pingedAt} capitalized={false} />)
          }

      ));
    }

    if (agent.connectionState === 'disconnected') {
      extras.push(this.renderExtraItem(
        'Disconnected',
        <FriendlyTime value={agent.disconnectedAt} />
      ));
    } else if (agent.connectionState === 'lost') {
      extras.push(this.renderExtraItem(
        'Lost',
        <FriendlyTime value={agent.lostAt} />
      ));
    } else if (agent.connectionState === 'stopped' || agent.connectionState === 'stopping') {
      extras.push(this.renderExtraItem(
        'Stopped',

          <FriendlyTime value={agent.stoppedAt} /> by {agent.stoppedBy.name}

      ));

      // Also show when the agent eventually disconnected
      if (agent.disconnectedAt) {
        extras.push(this.renderExtraItem(
          'Disconnected',
          <FriendlyTime value={agent.disconnectedAt} />
        ));
      }
    }

    return extras;
  }

  handleStopButtonClick = (evt) => {
    evt.preventDefault();

    this.setState({ stopping: true });

    const mutation = new AgentStopMutation({
      agent: this.props.agent,
      graceful: false
    });

    Relay.Store.commitUpdate(mutation, {
      onSuccess: this.handleMutationSuccess,
      onFailure: this.handleMutationError
    });
  };

  handleMutationSuccess = () => {
    this.setState({ stopping: false });
  };

  handleMutationError = (transaction) => {
    FlashesStore.flash(FlashesStore.ERROR, transaction.getError());

    this.setState({ stopping: false });
  };

  render() {
    let contents;
    if (this.props.relay.variables.isMounted) {
      contents = this.renderContents();
    } else {
      contents = this.renderSpinner();
    }

    return (
      <DocumentTitle title={`Agents / ${this.props.agent.name} · ${this.props.agent.organization.name}`}>
        <PageWithContainer>
          <Panel>
            <Panel.Header>{this.props.agent.name}</Panel.Header>
            {contents}
          </Panel>
        </PageWithContainer>
      </DocumentTitle>
    );
  }

  renderSpinner() {
    return (
      <Panel.Row>

          <Spinner />

      </Panel.Row>
    );
  }

  renderContents() {
    const agent = this.props.agent;
    const connectionStateClassName = getColourForConnectionState(agent.connectionState);

    let metaDataContent = 'None';
    if (agent.metaData && agent.metaData.length) {
      metaDataContent = agent.metaData.sort().join('\n');
    }

    return [
      <Panel.Row key="info">

          Status

            {getLabelForConnectionState(agent.connectionState)}

              {this.renderExtras(agent)}

      </Panel.Row>,

      <Panel.Row key="meta-data">

          Meta Data

{metaDataContent}

            You can use the agent’s meta-data to target the agent in your pipeline’s step configuration, or to set the agent’s queue.
            See the Agent Meta-data Documentation and Agent Queues Documentation for more details.

      </Panel.Row>,

      this.renderStopRow()
    ];
  }

  renderStopRow() {
    if (this.props.agent.connectionState !== 'connected') {
      return null;
    }

    return permissions(this.props.agent.permissions).collect(
      {
        allowed: "agentStop",
        render: (idx) => (
          <Panel.Row key={idx}>

              <Button
                theme="default"
                outline={true}
                loading={this.state.stopping ? "Stopping…" : false}
                onClick={this.handleStopButtonClick}
              >
                Stop Agent
              </Button>

                Remotely stop this agent process.
                Any running build job will be canceled.

          </Panel.Row>
        )
      }
    );
  }
}

// The $isMounted gear here is a little crappy. It should be nicer, but looks
// like there's a bug in grpahql-ruby at the moment:
// https://github.com/rmosolgo/graphql-ruby/issues/518

export default Relay.createContainer(AgentShow, {
  initialVariables: {
    isMounted: false
  },

  fragments: {
    agent: () => Relay.QL`
      fragment on Agent {
        ${AgentStopMutation.getFragment('agent')}
        name
        organization {
          name
          slug
        }
        connectedAt @include(if: $isMounted)
        connectionState @include(if: $isMounted)
        disconnectedAt @include(if: $isMounted)
        hostname @include(if: $isMounted)
        id @include(if: $isMounted)
        ipAddress @include(if: $isMounted)
        job @include(if: $isMounted) {
          ${JobLink.getFragment('job')}
        }
        lostAt @include(if: $isMounted)
        metaData @include(if: $isMounted)
        operatingSystem @include(if: $isMounted)
        permissions @include(if: $isMounted) {
          agentStop {
            allowed
            code
            message
          }
        }
        pid @include(if: $isMounted)
        pingedAt @include(if: $isMounted)
        stoppedAt @include(if: $isMounted)
        stoppedBy @include(if: $isMounted) {
          name
        }
        userAgent @include(if: $isMounted)
        uuid @include(if: $isMounted)
        version @include(if: $isMounted)
      }
    `
  }
});
<commit_msg>Handle agents that 404 on the agent show page<commit_after>import React from 'react';
import Relay from 'react-relay';
import DocumentTitle from 'react-document-title';
import { seconds } from 'metrick/duration';

import Button from '../shared/Button';
import FlashesStore from '../../stores/FlashesStore';
import FriendlyTime from "../shared/FriendlyTime";
import JobLink from '../shared/JobLink';
import PageWithContainer from '../shared/PageWithContainer';
import Panel from '../shared/Panel';
import Spinner from '../shared/Spinner';
import permissions from '../../lib/permissions';
import { getColourForConnectionState, getLabelForConnectionState } from './shared';

import AgentStopMutation from '../../mutations/AgentStop';

class AgentShow extends React.Component {
  static propTypes = {
    agent: React.PropTypes.shape({
      id: React.PropTypes.string,
      name: React.PropTypes.string,
      connectionState: React.PropTypes.string,
      permissions: React.PropTypes.shape({
        agentStop: React.PropTypes.shape({
          allowed: React.PropTypes.bool
        })
      })
    }),
    relay: React.PropTypes.object.isRequired
  };

  state = {
    stopping: false
  };

  componentDidMount() {
    // Only bother setting up the delayed load and refresher if we've got an
    // actual agent to play with.
    if(this.props.agent && this.props.agent.id) {
      this._agentRefreshInterval = setInterval(this.fetchUpdatedData, 5::seconds);

      // Once the agent's show page has mounted in DOM, switch `isMounted` to
      // true which will trigger a load of all the additional information we need
      // to show this page.
      this.props.relay.setVariables({ isMounted: true });
    }
  }

  componentWillUnmount() {
    if(this._agentRefreshInterval) {
      clearInterval(this._agentRefreshInterval);
    }
  }

  fetchUpdatedData = () => {
    this.props.relay.forceFetch(true);
  };

  renderExtraItem(title, content) {
    return (

        {title}: {content}

    );
  }

  renderExtras(agent) {
    const extras = [];

    if (agent.version) {
      extras.push(this.renderExtraItem('Version', agent.version));
    }

    if (agent.hostname) {
      extras.push(this.renderExtraItem('Hostname', agent.hostname));
    }

    if (agent.pid) {
      extras.push(this.renderExtraItem('PID', agent.pid));
    }

    if (agent.ipAddress) {
      extras.push(this.renderExtraItem('IP Address', agent.ipAddress));
    }

    if (agent.userAgent) {
      extras.push(this.renderExtraItem('User Agent', agent.userAgent));
    }

    if (agent.operatingSystem) {
      extras.push(this.renderExtraItem('OS', agent.operatingSystem));
    }

    if (agent.priority) {
      extras.push(this.renderExtraItem('Priority', agent.priority));
    }

    if (agent.job) {
      extras.push(this.renderExtraItem('Running', <JobLink job={agent.job} />));
    }

    if (agent.connectedAt) {
      extras.push(this.renderExtraItem(
        'Connected',

          <FriendlyTime value={agent.connectedAt} />
          {agent.pingedAt && agent.connectionState === 'connected' &&
             (last check-in was <FriendlyTime value={agent.pingedAt} capitalized={false} />)
          }

      ));
    }

    if (agent.connectionState === 'disconnected') {
      extras.push(this.renderExtraItem(
        'Disconnected',
        <FriendlyTime value={agent.disconnectedAt} />
      ));
    } else if (agent.connectionState === 'lost') {
      extras.push(this.renderExtraItem(
        'Lost',
        <FriendlyTime value={agent.lostAt} />
      ));
    } else if (agent.connectionState === 'stopped' || agent.connectionState === 'stopping') {
      extras.push(this.renderExtraItem(
        'Stopped',

          <FriendlyTime value={agent.stoppedAt} /> by {agent.stoppedBy.name}

      ));

      // Also show when the agent eventually disconnected
      if (agent.disconnectedAt) {
        extras.push(this.renderExtraItem(
          'Disconnected',
          <FriendlyTime value={agent.disconnectedAt} />
        ));
      }
    }

    return extras;
  }

  handleStopButtonClick = (evt) => {
    evt.preventDefault();

    this.setState({ stopping: true });

    const mutation = new AgentStopMutation({
      agent: this.props.agent,
      graceful: false
    });

    Relay.Store.commitUpdate(mutation, {
      onSuccess: this.handleMutationSuccess,
      onFailure: this.handleMutationError
    });
  };

  handleMutationSuccess = () => {
    this.setState({ stopping: false });
  };

  handleMutationError = (transaction) => {
    FlashesStore.flash(FlashesStore.ERROR, transaction.getError());

    this.setState({ stopping: false });
  };

  render() {
    // If we don't have an agent object, or we do but it doesn't have an id
    // (perhaps Relay gave us an object but it's empty) then we can safely
    // assume that it's a 404.
    if(!this.props.agent || !this.props.agent.id) {
      return (
        <DocumentTitle title={`Agents / No Agent Found`}>
          <PageWithContainer>

No agent could be found!

          </PageWithContainer>
        </DocumentTitle>
      );
    }

    let contents;
    if (this.props.relay.variables.isMounted) {
      contents = this.renderContents();
    } else {
      contents = this.renderSpinner();
    }

    return (
      <DocumentTitle title={`Agents / ${this.props.agent.name} · ${this.props.agent.organization.name}`}>
        <PageWithContainer>
          <Panel>
            <Panel.Header>{this.props.agent.name}</Panel.Header>
            {contents}
          </Panel>
        </PageWithContainer>
      </DocumentTitle>
    );
  }

  renderSpinner() {
    return (
      <Panel.Row>

          <Spinner />

      </Panel.Row>
    );
  }

  renderContents() {
    const agent = this.props.agent;
    const connectionStateClassName = getColourForConnectionState(agent.connectionState);

    let metaDataContent = 'None';
    if (agent.metaData && agent.metaData.length) {
      metaDataContent = agent.metaData.sort().join('\n');
    }

    return [
      <Panel.Row key="info">

          Status

            {getLabelForConnectionState(agent.connectionState)}

              {this.renderExtras(agent)}

      </Panel.Row>,

      <Panel.Row key="meta-data">

          Meta Data

{metaDataContent}

            You can use the agent’s meta-data to target the agent in your pipeline’s step configuration, or to set the agent’s queue.
            See the Agent Meta-data Documentation and Agent Queues Documentation for more details.

      </Panel.Row>,

      this.renderStopRow()
    ];
  }

  renderStopRow() {
    if (this.props.agent.connectionState !== 'connected') {
      return null;
    }

    return permissions(this.props.agent.permissions).collect(
      {
        allowed: "agentStop",
        render: (idx) => (
          <Panel.Row key={idx}>

              <Button
                theme="default"
                outline={true}
                loading={this.state.stopping ? "Stopping…" : false}
                onClick={this.handleStopButtonClick}
              >
                Stop Agent
              </Button>

                Remotely stop this agent process.
                Any running build job will be canceled.

          </Panel.Row>
        )
      }
    );
  }
}

// The $isMounted gear here is a little crappy. It should be nicer, but looks
// like there's a bug in grpahql-ruby at the moment:
// https://github.com/rmosolgo/graphql-ruby/issues/518

export default Relay.createContainer(AgentShow, {
  initialVariables: {
    isMounted: false
  },

  fragments: {
    agent: () => Relay.QL`
      fragment on Agent {
        ${AgentStopMutation.getFragment('agent')}
        id
        name
        organization {
          name
          slug
        }
        connectedAt @include(if: $isMounted)
        connectionState @include(if: $isMounted)
        disconnectedAt @include(if: $isMounted)
        hostname @include(if: $isMounted)
        id @include(if: $isMounted)
        ipAddress @include(if: $isMounted)
        job @include(if: $isMounted) {
          ${JobLink.getFragment('job')}
        }
        lostAt @include(if: $isMounted)
        metaData @include(if: $isMounted)
        operatingSystem @include(if: $isMounted)
        permissions @include(if: $isMounted) {
          agentStop {
            allowed
            code
            message
          }
        }
        pid @include(if: $isMounted)
        pingedAt @include(if: $isMounted)
        stoppedAt @include(if: $isMounted)
        stoppedBy @include(if: $isMounted) {
          name
        }
        userAgent @include(if: $isMounted)
        uuid @include(if: $isMounted)
        version @include(if: $isMounted)
      }
    `
  }
});
<|endoftext|>


<commit_before>/*
TODO
- Check whether files exist before uploading (will always overwrite as-is)
- Support multiple retry attempts if a file exists (see FS Adapter)
*/

// Mirroring keystone 0.4's support of node 0.12.
var fs = require('fs');
var pathlib = require('path');
var assign = require('object-assign');
var debug = require('debug')('keystone-s3');
var ensureCallback = require('keystone-storage-namefunctions/ensureCallback');
var nameFunctions = require('keystone-storage-namefunctions');
var S3 = require('aws-sdk/clients/s3');

var DEFAULT_OPTIONS = {
	key: process.env.S3_KEY,
	secret: process.env.S3_SECRET,
	bucket: process.env.S3_BUCKET,
	region: process.env.S3_REGION || 'us-east-1',
	path: '/',
	generateFilename: nameFunctions.randomFilename,
	uploadParams: {},
};

function ensureLeadingSlash (filename) {
	return filename[0] !== '/' ? '/' + filename : filename;
}

function removeLeadingSlash (filename) {
	return filename[0] === '/' ? filename.substring(1) : filename;
}

function encodeSpecialCharacters (filename) {
	// Note: these characters are valid in URIs, but S3 does not like them for
	// some reason.
	return encodeURI(filename).replace(/[!'()#*+? ]/g, function (char) {
		return '%' + char.charCodeAt(0).toString(16);
	});
}

// This constructor is usually called indirectly by the Storage class
// in keystone.

// S3-specific options should be specified in an `options.s3` field,
// which can contain the following options: { key, secret, bucket, region,
// path, uploadParams, publicUrl }.

// The schema can contain the additional fields { path, bucket, etag }.

// See README.md for details and usage examples.

function S3Adapter (options, schema) {
	this.options = assign({}, DEFAULT_OPTIONS, options.s3);

	// Check that `uploadParams` does not include any that we will be setting.
	var restrictedPrams = ['Key', 'Body', 'Bucket', 'ContentType'];
	Object.keys(this.options.uploadParams).forEach(function (key) {
		if (restrictedPrams.indexOf(key) !== -1) {
			throw new Error('Configuration error: `' + key + '` must not be set on `uploadParams`.');
		}
	});

	// Ensure the path has a leading "/"
	this.options.path = ensureLeadingSlash(this.options.path);

	// Create the s3 client
	this.s3Client = new S3({
		accessKeyId: this.options.key,
		secretAccessKey: this.options.secret,
		region: this.options.region,
	});

	// Ensure the generateFilename option takes a callback
	this.options.generateFilename = ensureCallback(this.options.generateFilename);
}

S3Adapter.compatibilityLevel = 1;

// All the extra schema fields supported by this adapter.
S3Adapter.SCHEMA_TYPES = {
	filename: String,
	bucket: String,
	path: String,
	etag: String,
};

S3Adapter.SCHEMA_FIELD_DEFAULTS = {
	filename: true,
	bucket: false,
	path: false,
	etag: false,
};

S3Adapter.prototype._resolveBucket = function (file) {
	if (file && file.bucket && file.bucket) {
		return file.bucket;
	} else {
		return this.options.bucket;
	}
};

S3Adapter.prototype._resolvePath = function (file) {
	// Just like the bucket, the schema can store the path for files. If the path
	// isn't stored we'll assume all the files are in the path specified in the
	// s3.path option which defaults to the root of the bucket.
	const path = (file && file.path) || this.options.path;
	// We still need to ensureLeadingSlash here as older versions of this
	// adapter did not so there may be bad data for file.path in the DB.
	return ensureLeadingSlash(path);
};

// Get the full, absolute path name for the specified file.
S3Adapter.prototype._resolveAbsolutePath = function (file) {
	var path = this._resolvePath(file);
	var filename = pathlib.posix.resolve(path, file.filename);
	return encodeSpecialCharacters(filename);
};

S3Adapter.prototype.uploadFile = function (file, callback) {
	var self = this;
	this.options.generateFilename(file, 0, function (err, filename) {
		if (err) return callback(err);

		// The expanded path of the file on the filesystem.
		var localpath = file.path;
		// Grab the mimetype so we can set ContentType in S3
		var mimetype = file.mimetype;

		// The destination path inside the S3 bucket.
		file.path = self.options.path;
		file.filename = filename;
		var absolutePath = self._resolveAbsolutePath(file);
		var bucket = self._resolveBucket();

		debug('Uploading file "%s" to "%s" bucket with mimetype "%s"', absolutePath, bucket, mimetype);

		var fileStream = fs.createReadStream(localpath);
		fileStream.on('error', function (err) {
			if (err) return callback(err);
		});

		var params = assign({
			Key: removeLeadingSlash(absolutePath),
			Body: fileStream,
			Bucket: bucket,
			ContentType: mimetype,
		}, self.options.uploadParams);

		self.s3Client.upload(params, function (err, data) {
			if (err) return callback(err);
			// We'll annotate the file with a bunch of extra properties. These won't
			// be saved in the database unless the corresponding schema options are
			// set.
			file.filename = filename;
			// NOTE: The etag is double-quoted. This is correct because an ETag
			// according to the spec is either a quoted-string or W/ followed by
			// a quoted-string (so, for example W/"asdf" is a valid etag).
			// https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
			file.etag = data.ETag;

			// file.url is automatically populated by keystone's Storage class so we
			// don't need to set it here.

			// The path and bucket can be stored on a per-file basis if you want.
			// The effect of this is that you can have some (eg, old) files in your
			// collection stored in different bucket / different path inside your
			// bucket. This means you can do slow data migrations. Note that if you
			// *don't* store these values you can arguably migrate your data more
			// easily - just move it all, reconfigure and restart your server.
			file.path = self.options.path;
			file.bucket = self.options.bucket;

			debug('file upload successful %s', absolutePath);
			callback(null, file);
		});
	});
};

// Note that this will provide a public URL for the file, but it will only
// work if:
// - the bucket is public (best) or
// - the file is set to a canned ACL (ie, uploadParams:{ ACL: 'public-read' } )
// - you pass credentials during your request for the file content itself
S3Adapter.prototype.getFileURL = function (file) {
	var absolutePath = this._resolveAbsolutePath(file);
	var bucket = this._resolveBucket(file);

	if (typeof this.options.publicUrl === 'string') {
		return this.options.publicUrl + absolutePath;
	}
	if (typeof this.options.publicUrl === 'function') {
		return this.options.publicUrl(file);
	}
	return 'https://' + bucket + '.s3.amazonaws.com' + absolutePath;
};

S3Adapter.prototype.removeFile = function (file, callback) {
	var absolutePath = this._resolveAbsolutePath(file);
	var bucket = this._resolveBucket(file);

	debug('Removing file "%s" from "%s" bucket', absolutePath, bucket);

	var params = {
		Key: removeLeadingSlash(absolutePath),
		Bucket: bucket,
	};

	this.s3Client.deleteObject(params, function (err, data) {
		if (err) return callback(err);
		callback();
	});
};

// Check if a file with the specified filename already exists. Callback called
// with the file headers if the file exists, null otherwise.
S3Adapter.prototype.fileExists = function (filename, callback) {
	var absolutePath = this._resolveAbsolutePath({ filename: filename });
	var bucket = this._resolveBucket();

	debug('Checking file exists "%s" in "%s" bucket', absolutePath, bucket);

	var params = {
		Key: removeLeadingSlash(absolutePath),
		Bucket: bucket,
	};

	this.s3Client.getObject(params, function (err, data) {
		if (err) return callback(err);
		else callback(null, data);
	});
};

module.exports = S3Adapter;
<commit_msg>add ContentLength param<commit_after>/*
TODO
- Check whether files exist before uploading (will always overwrite as-is)
- Support multiple retry attempts if a file exists (see FS Adapter)
*/

// Mirroring keystone 0.4's support of node 0.12.
var fs = require('fs');
var pathlib = require('path');
var assign = require('object-assign');
var debug = require('debug')('keystone-s3');
var ensureCallback = require('keystone-storage-namefunctions/ensureCallback');
var nameFunctions = require('keystone-storage-namefunctions');
var S3 = require('aws-sdk/clients/s3');

var DEFAULT_OPTIONS = {
	key: process.env.S3_KEY,
	secret: process.env.S3_SECRET,
	bucket: process.env.S3_BUCKET,
	region: process.env.S3_REGION || 'us-east-1',
	path: '/',
	generateFilename: nameFunctions.randomFilename,
	uploadParams: {},
};

function ensureLeadingSlash (filename) {
	return filename[0] !== '/' ? '/' + filename : filename;
}

function removeLeadingSlash (filename) {
	return filename[0] === '/' ? filename.substring(1) : filename;
}

function encodeSpecialCharacters (filename) {
	// Note: these characters are valid in URIs, but S3 does not like them for
	// some reason.
	return encodeURI(filename).replace(/[!'()#*+? ]/g, function (char) {
		return '%' + char.charCodeAt(0).toString(16);
	});
}

// This constructor is usually called indirectly by the Storage class
// in keystone.

// S3-specific options should be specified in an `options.s3` field,
// which can contain the following options: { key, secret, bucket, region,
// path, uploadParams, publicUrl }.

// The schema can contain the additional fields { path, bucket, etag }.

// See README.md for details and usage examples.

function S3Adapter (options, schema) {
	this.options = assign({}, DEFAULT_OPTIONS, options.s3);

	// Check that `uploadParams` does not include any that we will be setting.
	var restrictedPrams = ['Key', 'Body', 'Bucket', 'ContentType', 'ContentLength'];
	Object.keys(this.options.uploadParams).forEach(function (key) {
		if (restrictedPrams.indexOf(key) !== -1) {
			throw new Error('Configuration error: `' + key + '` must not be set on `uploadParams`.');
		}
	});

	// Ensure the path has a leading "/"
	this.options.path = ensureLeadingSlash(this.options.path);

	// Create the s3 client
	this.s3Client = new S3({
		accessKeyId: this.options.key,
		secretAccessKey: this.options.secret,
		region: this.options.region,
	});

	// Ensure the generateFilename option takes a callback
	this.options.generateFilename = ensureCallback(this.options.generateFilename);
}

S3Adapter.compatibilityLevel = 1;

// All the extra schema fields supported by this adapter.
S3Adapter.SCHEMA_TYPES = {
	filename: String,
	bucket: String,
	path: String,
	etag: String,
};

S3Adapter.SCHEMA_FIELD_DEFAULTS = {
	filename: true,
	bucket: false,
	path: false,
	etag: false,
};

S3Adapter.prototype._resolveBucket = function (file) {
	if (file && file.bucket && file.bucket) {
		return file.bucket;
	} else {
		return this.options.bucket;
	}
};

S3Adapter.prototype._resolvePath = function (file) {
	// Just like the bucket, the schema can store the path for files. If the path
	// isn't stored we'll assume all the files are in the path specified in the
	// s3.path option which defaults to the root of the bucket.
	const path = (file && file.path) || this.options.path;
	// We still need to ensureLeadingSlash here as older versions of this
	// adapter did not so there may be bad data for file.path in the DB.
	return ensureLeadingSlash(path);
};

// Get the absolute path name for the specified file.
S3Adapter.prototype._resolveAbsolutePath = function (file) {
	var path = this._resolvePath(file);
	var filename = pathlib.posix.resolve(path, file.filename);
	return encodeSpecialCharacters(filename);
};

S3Adapter.prototype.uploadFile = function (file, callback) {
	var self = this;
	this.options.generateFilename(file, 0, function (err, filename) {
		if (err) return callback(err);

		// The expanded path of the file on the filesystem.
		var localpath = file.path;
		// Grab the mimetype so we can set ContentType in S3
		var mimetype = file.mimetype;
		// Grab the size so we can set ContentLength
		var filesize = file.size;

		// The destination path inside the S3 bucket.
		file.path = self.options.path;
		file.filename = filename;
		var absolutePath = self._resolveAbsolutePath(file);
		var bucket = self._resolveBucket();

		debug('Uploading file "%s" to "%s" bucket with mimetype "%s"', absolutePath, bucket, mimetype);

		var fileStream = fs.createReadStream(localpath);
		fileStream.on('error', function (err) {
			if (err) return callback(err);
		});

		var params = assign({
			Key: removeLeadingSlash(absolutePath),
			Body: fileStream,
			Bucket: bucket,
			ContentType: mimetype,
			ContentLength: filesize,
		}, self.options.uploadParams);

		self.s3Client.upload(params, function (err, data) {
			if (err) return callback(err);
			// We'll annotate the file with a bunch of extra properties. These won't
			// be saved in the database unless the corresponding schema options are
			// set.
			file.filename = filename;
			// NOTE: The etag is double-quoted. This is correct because an ETag
			// according to the spec is either a quoted-string or W/ followed by
			// a quoted-string (so, for example W/"asdf" is a valid etag).
			// https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
			file.etag = data.ETag;

			// file.url is automatically populated by keystone's Storage class so we
			// don't need to set it here.

			// The path and bucket can be stored on a per-file basis if you want.
			// The effect of this is that you can have some (eg, old) files in your
			// collection stored in different bucket / different path inside your
			// bucket. This means you can do slow data migrations. Note that if you
			// *don't* store these values you can arguably migrate your data more
			// easily - just move it all, reconfigure and restart your server.
			file.path = self.options.path;
			file.bucket = self.options.bucket;

			debug('file upload successful %s', absolutePath);
			callback(null, file);
		});
	});
};

// Note that this will provide a public URL for the file, but it will only
// work if:
// - the bucket is public (best) or
// - the file is set to a canned ACL (ie, uploadParams:{ ACL: 'public-read' } )
// - you pass credentials during your request for the file content itself
S3Adapter.prototype.getFileURL = function (file) {
	var absolutePath = this._resolveAbsolutePath(file);
	var bucket = this._resolveBucket(file);

	if (typeof this.options.publicUrl === 'string') {
		return this.options.publicUrl + absolutePath;
	}
	if (typeof this.options.publicUrl === 'function') {
		return this.options.publicUrl(file);
	}
	return 'https://' + bucket + '.s3.amazonaws.com' + absolutePath;
};

S3Adapter.prototype.removeFile = function (file, callback) {
	var absolutePath = this._resolveAbsolutePath(file);
	var bucket = this._resolveBucket(file);

	debug('Removing file "%s" from "%s" bucket', absolutePath, bucket);

	var params = {
		Key: removeLeadingSlash(absolutePath),
		Bucket: bucket,
	};

	this.s3Client.deleteObject(params, function (err, data) {
		if (err) return callback(err);
		callback();
	});
};

// Check if a file with the specified filename already exists. Callback called
// with the file headers if the file exists, null otherwise.
S3Adapter.prototype.fileExists = function (filename, callback) {
	var absolutePath = this._resolveAbsolutePath({ filename: filename });
	var bucket = this._resolveBucket();

	debug('Checking file exists "%s" in "%s" bucket', absolutePath, bucket);

	var params = {
		Key: removeLeadingSlash(absolutePath),
		Bucket: bucket,
	};

	this.s3Client.getObject(params, function (err, data) {
		if (err) return callback(err);
		else callback(null, data);
	});
};

module.exports = S3Adapter;
<|endoftext|>