<commit_before>/* jshint browser:true, globalstrict:true */
/* global angular:true */
"use strict";

angular.module('webPGQ.services')
    .service('graph', ['_', 'dagreD3', function(_, dagreD3)
    {
        var edgeWidthKey, maxEdgeWidthValue;
        var edgeLabelTemplate;
        var lastNodeDefID = 0;
        var graph, topNodeID, lastPlans;

        var minEdgeWidth = 1, maxEdgeWidth = 40;
        var edgeWidthRange = maxEdgeWidth - minEdgeWidth;

        var nodeTypeIcons = [
            'Aggregate',
            'Append',
            'Bitmap Heap Scan',
            'Bitmap Index Scan',
            'Function Scan',
            'Hash Anti Join',
            'Hash Join',
            'Hash Setop Except',
            'Hash Setop Intersect',
            'Hash Setop Unknown',
            'Hash',
            'Index Only Scan',
            'Index Scan',
            'Limit',
            'Materialize',
            'Nested Loop',
            'Result',
            'Seq Scan',
            'Sort',
            'Window Agg'
        ];

        var initPlanRefRE = /\$\d+\b/g;
        var nodeRefRE = /\$\d+\b|\((CTE [^)]+|SubPlan \d+)\)/g;

        function getMaxEdgeWidthValue(plan)
        {
            return Math.max.apply(Math,
                [plan[edgeWidthKey] || 0]
                    .concat(
                        _.map(plan.Plans || [], getMaxEdgeWidthValue)
                    )
            );
        } // end getMaxEdgeWidthValue

        function updateEdge(parent, child, edge)
        {
            edge = edge || {};

            edge.label = edgeLabelTemplate({
                parent: parent,
                child: child,
                edgeWidthKey: edgeWidthKey
            }) || '<UNDEFINED>';

            // Calculate the edge size based on percentage of the maximum value of `edgeWidthKey`.
            var edgePercent = maxEdgeWidthValue ? (child[edgeWidthKey] || 0) / maxEdgeWidthValue : 0;
            var edgeSize = (edgePercent * edgeWidthRange) + minEdgeWidth;

            edge.style = (edge.style ? edge.style + ';' : '') + 'stroke-width: ' + edgeSize + 'px';

            return edge;
        } // end updateEdge

        var tmplDisplayRE = /\$\{child\['([^']*)'\]\}/g;
        function tmplDisplayReplacement(match, field)
        {
            return '[' + field + ']';
        } // end tmplDisplayReplacement

        function labelTmpl(tmplString)
        {
            var tmpl = _.template(tmplString);
            tmpl.template = tmplString;
            tmpl.display = tmplString.replace(tmplDisplayRE, tmplDisplayReplacement);
            return tmpl;
        } // end labelTmpl

        // Edge label templates, according to edgeWidthKey.
        var defaultEdgeLabel = _.template("${child[edgeWidthKey]}");
        var edgeLabels = {
            'Startup Cost': labelTmpl("${child['Startup Cost']}..${child['Total Cost']}"),
            'Total Cost': labelTmpl("${child['Startup Cost']}..${child['Total Cost']}"),
            'Actual Startup Time': labelTmpl("${child['Actual Startup Time']}..${child['Actual Total Time']}"),
            'Actual Total Time': labelTmpl("${child['Actual Startup Time']}..${child['Actual Total Time']}"),
        };

        function findReferences(node, key, val)
        {
            var match;
            if(key == 'Subplan Name')
            {
                if((match = initPlanRefRE.exec(val)) !== null)
                {
                    node.refName = match[0];
                }
                else
                {
                    node.refName = val;
                } // end if
            }
            else if(key == 'CTE Name')
            {
                node.references.push({name: 'CTE ' + val, field: key});
            }
            else
            {
                while((match = nodeRefRE.exec(val)) !== null)
                {
                    node.references.push({name: match[1] || match[0], field: key});
                } // end while
            } // end if
        } // end findReferences

        var nodeIDsByRef = {};

        var graphService = {
            nodesFromPlan: function(plan)
            {
                var metadata = {};
                var thisNode = {references: []};
                for(var key in plan)
                {
                    if(key != 'Plans')
                    {
                        var val = plan[key];
                        metadata[key] = val;

                        if(Array.isArray(val))
                        {
                            val.forEach(findReferences.bind(null, thisNode, key));
                        }
                        else
                        {
                            findReferences.call(null, thisNode, key, val);
                        } // end if
                    } // end if
                } // end for

                if(thisNode.references.length > 0)
                {
                    // Defer calculating `References` until later, after we've fully populated nodeIDsByRef.
                    var fullRefs;
                    Object.defineProperty(metadata, 'References', {
                        enumerable: true,
                        get: function()
                        {
                            if(!fullRefs)
                            {
                                fullRefs = thisNode.references.map(function(ref)
                                {
                                    ref.id = nodeIDsByRef[ref.name];
                                    return ref;
                                });
                            } // end if
                            return fullRefs;
                        }
                    });
                } // end if

                var useDef;
                if(nodeTypeIcons.indexOf(plan['Node Type']) != -1)
                {
                    useDef = 'node-def-' + (++lastNodeDefID);
                } // end if

                var thisNodeID = graph.addNode(null, {
                    label: plan['Node Type'] + (plan.Strategy ? ' ' + plan.Strategy : ''),
                    metadata: metadata,
                    useDef: useDef,
                    iconURL: 'icons/' + plan['Node Type'] + '.svg'
                });

                if(thisNode.refName)
                {
                    nodeIDsByRef[thisNode.refName] = thisNodeID;
                } // end if

                _.forEach(plan.Plans || [], function(childPlan, idx)
                {
                    var childInfo = graphService.nodesFromPlan(childPlan);

                    graph.addEdge(null, childInfo.id, thisNodeID,
                        updateEdge(plan, childInfo.planNode, { weight: idx === 0 ? 1 : 0 })
                    );
                });

                return {
                    id: thisNodeID,
                    planNode: _.omit(plan, 'Plans')
                };
            }, // end nodesFromPlan

            fromPlan: function(plans, edgeWidthKey_)
            {
                // Create a new directed graph
                graph = new dagreD3.Digraph();
                graph.marginx = 4;
                graph.marginy = 4;

                if(typeof plans == 'string')
                {
                    // PostgreSQL < 9.2 returns JSON plans as strings.
                    plans = JSON.parse(plans);
                } // end if

                lastPlans = plans;

                edgeWidthKey = edgeWidthKey_;
                edgeLabelTemplate = edgeLabels[edgeWidthKey] || defaultEdgeLabel;
                graph.edgeLabelLegend = edgeLabelTemplate.display;

                maxEdgeWidthValue = Math.max.apply(Math,
                    _.map(plans, function(plan) { return getMaxEdgeWidthValue(plan.Plan); })
                );

                var metadata = {};
                metadata['Maximum ' + edgeWidthKey] = maxEdgeWidthValue;
                topNodeID = graph.addNode(null, { label: 'Total', metadata: metadata });

                plans.forEach(function(plan, idx)
                {
                    var childInfo = graphService.nodesFromPlan(plan.Plan);
                    var childData = graph.node(childInfo.id);

                    // Copy top-level plan metadata to the plan's node metadata.
                    _.forOwn(plan, function(value, key)
                    {
                        if(key != 'Plan' && key[0] != '_')
                        {
                            if(childData.metadata[key])
                            {
                                childData.metadata['Plan ' + key] = value;
                            }
                            else
                            {
                                childData.metadata[key] = value;
                            } // end if
                        } // end if
                    });

                    graph.addEdge(null, childInfo.id, topNodeID,
                        updateEdge({}, childInfo.planNode, { weight: idx === 0 ? 1 : 0, style: 'stroke: #bbb' })
                    );
                });

                return graph;
            }, // end fromPlan

            updateEdges: function(edgeWidthKey_)
            {
                if(!graph)
                {
                    return;
                } // end if

                edgeWidthKey = edgeWidthKey_;
                edgeLabelTemplate = edgeLabels[edgeWidthKey] || defaultEdgeLabel;
                graph.edgeLabelLegend = edgeLabelTemplate.display;

                maxEdgeWidthValue = Math.max.apply(Math,
                    _.map(lastPlans, function(plan) { return getMaxEdgeWidthValue(plan.Plan); })
                );

                graph.node(topNodeID).metadata['Maximum ' + edgeWidthKey] = maxEdgeWidthValue;

                graph.eachEdge(function(id, u, v)
                {
                    var edge = graph.edge(id);

                    updateEdge(graph.node(v).metadata, graph.node(u).metadata, edge);
                });

                return graph;
            }, // end updateEdges
        }; // end graphService

        return graphService;
    }]);
<commit_msg>Made summary arrows striped.<commit_after>/* jshint browser:true, globalstrict:true */
/* global angular:true */
"use strict";

angular.module('webPGQ.services')
    .service('graph', ['_', 'dagreD3', function(_, dagreD3)
    {
        var edgeWidthKey, maxEdgeWidthValue;
        var edgeLabelTemplate;
        var lastNodeDefID = 0;
        var graph, topNodeID, lastPlans;

        var minEdgeWidth = 1, maxEdgeWidth = 40;
        var edgeWidthRange = maxEdgeWidth - minEdgeWidth;

        var nodeTypeIcons = [
            'Aggregate',
            'Append',
            'Bitmap Heap Scan',
            'Bitmap Index Scan',
            'Function Scan',
            'Hash Anti Join',
            'Hash Join',
            'Hash Setop Except',
            'Hash Setop Intersect',
            'Hash Setop Unknown',
            'Hash',
            'Index Only Scan',
            'Index Scan',
            'Limit',
            'Materialize',
            'Nested Loop',
            'Result',
            'Seq Scan',
            'Sort',
            'Window Agg'
        ];

        var initPlanRefRE = /\$\d+\b/g;
        var nodeRefRE = /\$\d+\b|\((CTE [^)]+|SubPlan \d+)\)/g;

        function getMaxEdgeWidthValue(plan)
        {
            return Math.max.apply(Math,
                [plan[edgeWidthKey] || 0]
                    .concat(
                        _.map(plan.Plans || [], getMaxEdgeWidthValue)
                    )
            );
        } // end getMaxEdgeWidthValue

        function updateEdge(parent, child, edge)
        {
            edge = edge || {};

            edge.label = edgeLabelTemplate({
                parent: parent,
                child: child,
                edgeWidthKey: edgeWidthKey
            }) || '<UNDEFINED>';

            // Calculate the edge size based on percentage of the maximum value of `edgeWidthKey`.
            var edgePercent = maxEdgeWidthValue ? (child[edgeWidthKey] || 0) / maxEdgeWidthValue : 0;
            var edgeSize = (edgePercent * edgeWidthRange) + minEdgeWidth;

            edge.style = (edge.style ? edge.style + ';' : '') + 'stroke-width: ' + edgeSize + 'px';

            return edge;
        } // end updateEdge

        var tmplDisplayRE = /\$\{child\['([^']*)'\]\}/g;
        function tmplDisplayReplacement(match, field)
        {
            return '[' + field + ']';
        } // end tmplDisplayReplacement

        function labelTmpl(tmplString)
        {
            var tmpl = _.template(tmplString);
            tmpl.template = tmplString;
            tmpl.display = tmplString.replace(tmplDisplayRE, tmplDisplayReplacement);
            return tmpl;
        } // end labelTmpl

        // Edge label templates, according to edgeWidthKey.
        var defaultEdgeLabel = _.template("${child[edgeWidthKey]}");
        var edgeLabels = {
            'Startup Cost': labelTmpl("${child['Startup Cost']}..${child['Total Cost']}"),
            'Total Cost': labelTmpl("${child['Startup Cost']}..${child['Total Cost']}"),
            'Actual Startup Time': labelTmpl("${child['Actual Startup Time']}..${child['Actual Total Time']}"),
            'Actual Total Time': labelTmpl("${child['Actual Startup Time']}..${child['Actual Total Time']}"),
        };

        function findReferences(node, key, val)
        {
            var match;
            if(key == 'Subplan Name')
            {
                if((match = initPlanRefRE.exec(val)) !== null)
                {
                    node.refName = match[0];
                }
                else
                {
                    node.refName = val;
                } // end if
            }
            else if(key == 'CTE Name')
            {
                node.references.push({name: 'CTE ' + val, field: key});
            }
            else
            {
                while((match = nodeRefRE.exec(val)) !== null)
                {
                    node.references.push({name: match[1] || match[0], field: key});
                } // end while
            } // end if
        } // end findReferences

        var nodeIDsByRef = {};

        var graphService = {
            nodesFromPlan: function(plan)
            {
                var metadata = {};
                var thisNode = {references: []};
                for(var key in plan)
                {
                    if(key != 'Plans')
                    {
                        var val = plan[key];
                        metadata[key] = val;

                        if(Array.isArray(val))
                        {
                            val.forEach(findReferences.bind(null, thisNode, key));
                        }
                        else
                        {
                            findReferences.call(null, thisNode, key, val);
                        } // end if
                    } // end if
                } // end for

                if(thisNode.references.length > 0)
                {
                    // Defer calculating `References` until later, after we've fully populated nodeIDsByRef.
                    var fullRefs;
                    Object.defineProperty(metadata, 'References', {
                        enumerable: true,
                        get: function()
                        {
                            if(!fullRefs)
                            {
                                fullRefs = thisNode.references.map(function(ref)
                                {
                                    ref.id = nodeIDsByRef[ref.name];
                                    return ref;
                                });
                            } // end if
                            return fullRefs;
                        }
                    });
                } // end if

                var useDef;
                if(nodeTypeIcons.indexOf(plan['Node Type']) != -1)
                {
                    useDef = 'node-def-' + (++lastNodeDefID);
                } // end if

                var thisNodeID = graph.addNode(null, {
                    label: plan['Node Type'] + (plan.Strategy ? ' ' + plan.Strategy : ''),
                    metadata: metadata,
                    useDef: useDef,
                    iconURL: 'icons/' + plan['Node Type'] + '.svg'
                });

                if(thisNode.refName)
                {
                    nodeIDsByRef[thisNode.refName] = thisNodeID;
                } // end if

                _.forEach(plan.Plans || [], function(childPlan, idx)
                {
                    var childInfo = graphService.nodesFromPlan(childPlan);

                    graph.addEdge(null, childInfo.id, thisNodeID,
                        updateEdge(plan, childInfo.planNode, { weight: idx === 0 ? 1 : 0 })
                    );
                });

                return {
                    id: thisNodeID,
                    planNode: _.omit(plan, 'Plans')
                };
            }, // end nodesFromPlan

            fromPlan: function(plans, edgeWidthKey_)
            {
                // Create a new directed graph
                graph = new dagreD3.Digraph();
                graph.marginx = 4;
                graph.marginy = 4;

                if(typeof plans == 'string')
                {
                    // PostgreSQL < 9.2 returns JSON plans as strings.
                    plans = JSON.parse(plans);
                } // end if

                lastPlans = plans;

                edgeWidthKey = edgeWidthKey_;
                edgeLabelTemplate = edgeLabels[edgeWidthKey] || defaultEdgeLabel;
                graph.edgeLabelLegend = edgeLabelTemplate.display;

                maxEdgeWidthValue = Math.max.apply(Math,
                    _.map(plans, function(plan) { return getMaxEdgeWidthValue(plan.Plan); })
                );

                var metadata = {};
                metadata['Maximum ' + edgeWidthKey] = maxEdgeWidthValue;
                topNodeID = graph.addNode(null, { label: 'Total', metadata: metadata });

                plans.forEach(function(plan, idx)
                {
                    var childInfo = graphService.nodesFromPlan(plan.Plan);
                    var childData = graph.node(childInfo.id);

                    // Copy top-level plan metadata to the plan's node metadata.
                    _.forOwn(plan, function(value, key)
                    {
                        if(key != 'Plan' && key[0] != '_')
                        {
                            if(childData.metadata[key])
                            {
                                childData.metadata['Plan ' + key] = value;
                            }
                            else
                            {
                                childData.metadata[key] = value;
                            } // end if
                        } // end if
                    });

                    graph.addEdge(null, childInfo.id, topNodeID,
                        updateEdge({}, childInfo.planNode, { weight: idx === 0 ? 1 : 0, style: 'stroke: #bbb; stroke-dasharray: 2%' })
                    );
                });

                return graph;
            }, // end fromPlan

            updateEdges: function(edgeWidthKey_)
            {
                if(!graph)
                {
                    return;
                } // end if

                edgeWidthKey = edgeWidthKey_;
                edgeLabelTemplate = edgeLabels[edgeWidthKey] || defaultEdgeLabel;
                graph.edgeLabelLegend = edgeLabelTemplate.display;

                maxEdgeWidthValue = Math.max.apply(Math,
                    _.map(lastPlans, function(plan) { return getMaxEdgeWidthValue(plan.Plan); })
                );

                graph.node(topNodeID).metadata['Maximum ' + edgeWidthKey] = maxEdgeWidthValue;

                graph.eachEdge(function(id, u, v)
                {
                    var edge = graph.edge(id);

                    updateEdge(graph.node(v).metadata, graph.node(u).metadata, edge);
                });

                return graph;
            }, // end updateEdges
        }; // end graphService

        return graphService;
    }]);
<|endoftext|>


<commit_before>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Application bootstrapper

var stringUtils = require('utils/string_utils');

module.exports = Em.Application.create({
  name: 'Ambari Web',
  rootElement: '#wrapper',

  store: DS.Store.create({
    revision: 4,
    adapter: DS.FixtureAdapter.create({
      simulateRemoteResponse: false
    })
  }),
  isAdmin: false,
  /**
   * return url prefix with number value of version of HDP stack
   */
  stackVersionURL:function(){
    var stackVersion = this.get('currentStackVersion') || this.get('defaultStackVersion');
    if(stackVersion.indexOf('HDPLocal') !== -1){
      return '/stacks/HDPLocal/version/' + stackVersion.replace(/HDPLocal-/g, '');
    }
    return '/stacks/HDP/version/' + stackVersion.replace(/HDP-/g, '');
  }.property('currentStackVersion'),

  /**
   * return url prefix with number value of version of HDP stack
   */
  stack2VersionURL:function(){
    var stackVersion = this.get('currentStackVersion') || this.get('defaultStackVersion');
    if(stackVersion.indexOf('HDPLocal') !== -1){
      return '/stacks2/HDPLocal/versions/' + stackVersion.replace(/HDPLocal-/g, '');
    }
    return '/stacks2/HDP/versions/' + stackVersion.replace(/HDP-/g, '');
  }.property('currentStackVersion'),
  clusterName: null,
  currentStackVersion: '',
  currentStackVersionNumber: function(){
    return this.get('currentStackVersion').replace(/HDP(Local)?-/, '');
  }.property('currentStackVersion'),
  isHadoop2Stack: function(){
    return (stringUtils.compareVersions(this.get('currentStackVersionNumber'), "2.0") === 1 ||
      stringUtils.compareVersions(this.get('currentStackVersionNumber'), "2.0") === 0)
  }.property('currentStackVersionNumber'),

  /**
   * If High Availability is enabled
   * Based on clusterStatus.isInstalled, stack version, SNameNode availability
   *
   * @type {Boolean}
   */
  isHaEnabled: function() {
    if (!this.get('isHadoop2Stack')) return false;
    return !this.HostComponent.find().someProperty('componentName', 'SECONDARY_NAMENODE');
  }.property('router.clusterController.isLoaded'),

  components: Ember.Object.create({
    reassignable: ['NAMENODE', 'SECONDARY_NAMENODE', 'JOBTRACKER', 'RESOURCEMANAGER'],
    restartable: ['APP_TIMELINE_SERVER'],
    deletable: ['SUPERVISOR', 'HBASE_MASTER'],
    rollinRestartAllowed: ["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER", "SUPERVISOR"],
    slaves: function() {
      return require('data/service_components').filter(function(component){
        return !component.isClient && !component.isMaster
      }).mapProperty('component_name').uniq().without("DASHBOARD");
    }.property().cacheable(),

    masters: function() {
      return require('data/service_components').filterProperty('isMaster', true).mapProperty('component_name').uniq();
    }.property().cacheable()
  })
});

/**
 * overwritten set method of Ember.View to avoid uncaught errors
 * when trying to set property of destroyed view
 */
Em.View.reopen({
  set: function(attr, value){
    if(!this.get('isDestroyed') && !this.get('isDestroying')){
      this._super(attr, value);
    } else {
      console.debug('Calling set on destroyed view');
    }
  }
});

/**
 * Ambari overrides the default date transformer.
 * This is done because of the non-standard data
 * sent. For example Nagios sends date as "12345678".
 * The problem is that it is a String and is represented
 * only in seconds whereas Javascript's Date needs
 * milliseconds representation.
 */
DS.attr.transforms.date = {
  from: function (serialized) {
    var type = typeof serialized;
    if (type === "string") {
      serialized = parseInt(serialized);
      type = typeof serialized;
    }
    if (type === "number") {
      // The number could be seconds or milliseconds.
      // If seconds, then multiplying with 1000 should still
      // keep it below the current time.
      if (serialized * 1000 < new Date().getTime()) {
        serialized = serialized * 1000;
      }
      return new Date(serialized);
    } else if (serialized === null || serialized === undefined) {
      // if the value is not present in the data,
      // return undefined, not null.
      return serialized;
    } else {
      return null;
    }
  },
  to: function (deserialized) {
    if (deserialized instanceof Date) {
      return deserialized.getTime();
    } else if (deserialized === undefined) {
      return undefined;
    } else {
      return null;
    }
  }
};

DS.attr.transforms.object = {
  from: function(serialized) {
    return Ember.none(serialized) ? null : Object(serialized);
  },

  to: function(deserialized) {
    return Ember.none(deserialized) ? null : Object(deserialized);
  }
};

<commit_msg>AMBARI-4534 Add ability to delete individual DataNode, TaskTracker, NodeManager, and RegionServer from Host Details page. (ababiichuk)<commit_after>/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Application bootstrapper

var stringUtils = require('utils/string_utils');

module.exports = Em.Application.create({
  name: 'Ambari Web',
  rootElement: '#wrapper',

  store: DS.Store.create({
    revision: 4,
    adapter: DS.FixtureAdapter.create({
      simulateRemoteResponse: false
    })
  }),
  isAdmin: false,
  /**
   * return url prefix with number value of version of HDP stack
   */
  stackVersionURL:function(){
    var stackVersion = this.get('currentStackVersion') || this.get('defaultStackVersion');
    if(stackVersion.indexOf('HDPLocal') !== -1){
      return '/stacks/HDPLocal/version/' + stackVersion.replace(/HDPLocal-/g, '');
    }
    return '/stacks/HDP/version/' + stackVersion.replace(/HDP-/g, '');
  }.property('currentStackVersion'),

  /**
   * return url prefix with number value of version of HDP stack
   */
  stack2VersionURL:function(){
    var stackVersion = this.get('currentStackVersion') || this.get('defaultStackVersion');
    if(stackVersion.indexOf('HDPLocal') !== -1){
      return '/stacks2/HDPLocal/versions/' + stackVersion.replace(/HDPLocal-/g, '');
    }
    return '/stacks2/HDP/versions/' + stackVersion.replace(/HDP-/g, '');
  }.property('currentStackVersion'),
  clusterName: null,
  currentStackVersion: '',
  currentStackVersionNumber: function(){
    return this.get('currentStackVersion').replace(/HDP(Local)?-/, '');
  }.property('currentStackVersion'),
  isHadoop2Stack: function(){
    return (stringUtils.compareVersions(this.get('currentStackVersionNumber'), "2.0") === 1 ||
      stringUtils.compareVersions(this.get('currentStackVersionNumber'), "2.0") === 0)
  }.property('currentStackVersionNumber'),

  /**
   * If High Availability is enabled
   * Based on clusterStatus.isInstalled, stack version, SNameNode availability
   *
   * @type {Boolean}
   */
  isHaEnabled: function() {
    if (!this.get('isHadoop2Stack')) return false;
    return !this.HostComponent.find().someProperty('componentName', 'SECONDARY_NAMENODE');
  }.property('router.clusterController.isLoaded'),

  components: Ember.Object.create({
    reassignable: ['NAMENODE', 'SECONDARY_NAMENODE', 'JOBTRACKER', 'RESOURCEMANAGER'],
    restartable: ['APP_TIMELINE_SERVER'],
    deletable: ['SUPERVISOR', 'HBASE_MASTER', 'DATANODE', 'TASKTRACKER', 'NODEMANAGER', 'HBASE_REGIONSERVER'],
    rollinRestartAllowed: ["DATANODE", "TASKTRACKER", "NODEMANAGER", "HBASE_REGIONSERVER", "SUPERVISOR"],
    slaves: function() {
      return require('data/service_components').filter(function(component){
        return !component.isClient && !component.isMaster
      }).mapProperty('component_name').uniq().without("DASHBOARD");
    }.property().cacheable(),

    masters: function() {
      return require('data/service_components').filterProperty('isMaster', true).mapProperty('component_name').uniq();
    }.property().cacheable()
  })
});

/**
 * overwritten set method of Ember.View to avoid uncaught errors
 * when trying to set property of destroyed view
 */
Em.View.reopen({
  set: function(attr, value){
    if(!this.get('isDestroyed') && !this.get('isDestroying')){
      this._super(attr, value);
    } else {
      console.debug('Calling set on destroyed view');
    }
  }
});

/**
 * Ambari overrides the default date transformer.
 * This is done because of the non-standard data
 * sent. For example Nagios sends date as "12345678".
 * The problem is that it is a String and is represented
 * only in seconds whereas Javascript's Date needs
 * milliseconds representation.
 */
DS.attr.transforms.date = {
  from: function (serialized) {
    var type = typeof serialized;
    if (type === "string") {
      serialized = parseInt(serialized);
      type = typeof serialized;
    }
    if (type === "number") {
      // The number could be seconds or milliseconds.
      // If seconds, then multiplying with 1000 should still
      // keep it below the current time.
      if (serialized * 1000 < new Date().getTime()) {
        serialized = serialized * 1000;
      }
      return new Date(serialized);
    } else if (serialized === null || serialized === undefined) {
      // if the value is not present in the data,
      // return undefined, not null.
      return serialized;
    } else {
      return null;
    }
  },
  to: function (deserialized) {
    if (deserialized instanceof Date) {
      return deserialized.getTime();
    } else if (deserialized === undefined) {
      return undefined;
    } else {
      return null;
    }
  }
};

DS.attr.transforms.object = {
  from: function(serialized) {
    return Ember.none(serialized) ? null : Object(serialized);
  },

  to: function(deserialized) {
    return Ember.none(deserialized) ? null : Object(deserialized);
  }
};

<|endoftext|>


<commit_before>'use strict';

// Depends on js-yaml and underscore
// ToDo: Lazy load those dependencies into this component
// rather than hope the appropriate html script tag is used by app

angular.module('RepoFetcherMeta', ['GithubRepoFetcher', 'AngularEtag'])

  .factory('RepoMeta', function ($q, ehttp, GithubRepo, qChain, $window) {

    //ToDo: Consider making as a config
    var REPO_META_FILENAME = '.repo-meta.yml';
    var REPO_META_KEYNAME  = '_ff_meta_';

    function isRequestProblem(repoObj){
      if (!repoObj){ return {error: 'No repository information provided'}}
      if (!repoObj.contents_url){ return {error: 'Repository information did not have contents_url property'}}
      return false;
    }

    function getContentUrl(repoObj){

      var isProb = isRequestProblem(repoObj);

      //returns if there is a problem
      if (isProb){ return isProb }

      var urlTemplate = repoObj.contents_url;
      var path = REPO_META_FILENAME;
      var contentUrl = urlTemplate.replace(/{\+path}/, path);

      return  contentUrl;
    }

    function convertYmlCollection(ymlStrs){
      return ymlStrs.map( function(ymlStr){
        try {
          return jsyaml.safeLoad(ymlStr);
        } catch(e) {
          if(e.name === 'YAMLException'){
            return {error: e};
          } else {
          throw e;
          }
        }
      });
    }

    function isResponseProblem(respData){
      if(!respData || !respData.type ){
        return { error: 'Could not parse response'};
      }
      if(respData.type !== "file"){
        return { error: REPO_META_FILENAME + ' was not a file, it was' + respData.type.toString()};
      }

      if (respData.encoding && respData.encoding !== 'base64'){
        return { error: 'Unknown encoding for ' + REPO_META_FILENAME + ': ' + respData.encoding.toString() };
      }

      if (!respData.content){

        return { error: 'Could not find encoded content in ' + REPO_META_FILENAME };
      }

      return false

    }

    function makeYamlFromObj(jsObj){
      return '---\n' + jsyaml.safeDump(jsObj, {skipInvalid: true});
    }

    function getHttpDataAsString(url){

      if(typeof url !== 'string'){
        if(url.error){ return makeYamlFromObj(url); }
        var errObj = {error: 'Unknown URL type: ' + url.toString() };
        return makeYamlFromObj(errObj);
      }

      var getOpts = {
        url: url
        //transformResponse: function(data){return data}
      }

      //ToDo: have consistent error handling. Catch the error in the promise or no?
      return ehttp.etagGet(getOpts)
        .then( function(resp){
          var respData = resp.data;
          var isProblem = isResponseProblem(respData);
          if (isProblem){ return makeYamlFromObj(isProblem); }
          var respYaml = $window.atob(respData.content);
          return(respYaml);
        })

        // We don't want to quit fetching other repos
        // if one errors out. Instead return YAML with the error
        .catch(function(err){

          //we need to clean the error object up
          //so it's safe and parseable by Yaml
          //add optional yaml separator too ('---')
          var errObj = {error: err};
          return makeYamlFromObj(errObj);
        });
    }

    function insertRepoMeta(repos){
      //var contentUrls= repos.map( getContentUrl )
      var metaUrls = repos.map(function(repo){ return getContentUrl(repo) });
      //ToDo: add throttle mechanism

      return $q.all( metaUrls.map( getHttpDataAsString ) )
        .then(function(metaYmlStrs){
          var metaObjs = convertYmlCollection(metaYmlStrs);
          repos.forEach(function(repo, idx){
            repo[REPO_META_KEYNAME] = metaObjs[idx];
          });
          return repos;
      });
    }

    return {
      insertRepoMeta: insertRepoMeta
    };
  });
;
<commit_msg>atob fails in browser (not node) - debugging<commit_after>'use strict';

// Depends on js-yaml and underscore
// ToDo: Lazy load those dependencies into this component
// rather than hope the appropriate html script tag is used by app

angular.module('RepoFetcherMeta', ['GithubRepoFetcher', 'AngularEtag'])

  .factory('RepoMeta', function ($q, ehttp, GithubRepo, qChain, $window) {

    //ToDo: Consider making as a config
    var REPO_META_FILENAME = '.repo-meta.yml';
    var REPO_META_KEYNAME  = '_ff_meta_';

    function isRequestProblem(repoObj){
      if (!repoObj){ return {error: 'No repository information provided'}}
      if (!repoObj.contents_url){ return {error: 'Repository information did not have contents_url property'}}
      return false;
    }

    function getContentUrl(repoObj){

      var isProb = isRequestProblem(repoObj);

      //returns if there is a problem
      if (isProb){ return isProb }

      var urlTemplate = repoObj.contents_url;
      var path = REPO_META_FILENAME;
      var contentUrl = urlTemplate.replace(/{\+path}/, path);

      return  contentUrl;
    }

    function convertYmlCollection(ymlStrs){
      return ymlStrs.map( function(ymlStr){
        try {
          return jsyaml.safeLoad(ymlStr);
        } catch(e) {
          if(e.name === 'YAMLException'){
            return {error: e};
          } else {
          throw e;
          }
        }
      });
    }

    function isResponseProblem(respData){
      if(!respData || !respData.type ){
        return { error: 'Could not parse response'};
      }
      if(respData.type !== "file"){
        return { error: REPO_META_FILENAME + ' was not a file, it was' + respData.type.toString()};
      }

      if (respData.encoding && respData.encoding !== 'base64'){
        return { error: 'Unknown encoding for ' + REPO_META_FILENAME + ': ' + respData.encoding.toString() };
      }

      if (!respData.content){

        return { error: 'Could not find encoded content in ' + REPO_META_FILENAME };
      }

      return false

    }

    function makeYamlFromObj(jsObj){
      return '---\n' + jsyaml.safeDump(jsObj, {skipInvalid: true});
    }

    function getHttpDataAsString(url){

      if(typeof url !== 'string'){
        if(url.error){ return makeYamlFromObj(url); }
        var errObj = {error: 'Unknown URL type: ' + url.toString() };
        return makeYamlFromObj(errObj);
      }

      var getOpts = {
        url: url
        //transformResponse: function(data){return data}
      }

      //ToDo: have consistent error handling. Catch the error in the promise or no?
      return ehttp.etagGet(getOpts)
        .then( function(resp){
          var respData = resp.data;
          var isProblem = isResponseProblem(respData);
          if (isProblem){ return makeYamlFromObj(isProblem); }
          var respYaml;
          try {
            respYaml = $window.atob(respData.content);
          } catch(e) {
            respYaml = makeYamlFromObj({error: e});
          }

          return(respYaml);
        })

        // We don't want to quit fetching other repos
        // if one errors out. Instead return YAML with the error
        .catch(function(err){

          //we need to clean the error object up
          //so it's safe and parseable by Yaml
          //add optional yaml separator too ('---')
          var errObj = {error: err};
          return makeYamlFromObj(errObj);
        });
    }

    function insertRepoMeta(repos){
      //var contentUrls= repos.map( getContentUrl )
      var metaUrls = repos.map(function(repo){ return getContentUrl(repo) });
      //ToDo: add throttle mechanism

      return $q.all( metaUrls.map( getHttpDataAsString ) )
        .then(function(metaYmlStrs){
          var metaObjs = convertYmlCollection(metaYmlStrs);
          repos.forEach(function(repo, idx){
            repo[REPO_META_KEYNAME] = metaObjs[idx];
          });
          return repos;
      });
    }

    return {
      insertRepoMeta: insertRepoMeta
    };
  });
;
<|endoftext|>


<commit_before>var pg = require('pg');
var config = require('../config');

exports = module.exports = function(req, res) {

  var databaseTools = {
    database: function (callback) {
      var connectionString = [
        'postgres://',
        config.database.username,
        ':',
        config.database.password,
        '@',
        config.database.address,
        '/',config.database.name
      ].join('');
      pg.connect(connectionString, callback);
    },
    addParams: function (query, type) {
      // TODO: This should use handlebars or something like it
      var newQuery = query,
      re = function(name) {return new RegExp('{{'+name+'}}','g');},
      param;
      for (param in req.params) {
        newQuery = newQuery.replace(re(param), req.params[param]);
      }
      newQuery = newQuery.replace(re('&type&'), type);
      console.log(newQuery);
      return newQuery;
    },
    query: function(query, type, callback) {
      var queryResult = [];
      databaseTools.database(function(err, client, done) {
        if (err) {
          queryResult.error = {'code': '500'};
          queryResult.details = err;
          callback(res, queryResult);
        } else {
          client.query(databaseTools.addParams(query, type), function(err, results) {
            if (err) {
              queryResult.error = {'code': '500'};
              queryResult.details = err;
            } else {
              queryResult.data = databaseTools.parse(results, type);
            }
            callback(res, queryResult);
            done();
          });
        }
      });
    },
    translateField: {
      '17987': function (hstore) {
        //hstore
        var returnValue = [];

        hstore.split('", "').map(function (tag) {
          var tags = tag.split('=>');
          returnValue.push({'k': tags[0].replace(/"$|^"/g, ''), 'v': tags[1].replace(/"$|^"/g, '')});
        });

        return returnValue;
      },
      '25846': function(hstore) {return databaseTools.translateField['17987'](hstore);},
      '114': function(json) {
        return json;
      },
      '1184': function (timestamp){
        // timestamp
        return timestamp.toISOString().replace('.000','');
      },
      '1016': function (ints) {
        //Array of ints
        var returnValue = [];

        ints.toString().split(',').map(function (val) {
          returnValue.push({'ref': val});
        });

        return returnValue;
      }
    },
    parse: function(results, type) {
      var returnValue = {}, returnRow;
      returnValue[type] = [];

      results.rows.map(function(row) {
        returnRow = {};
        results.fields.map(function(field){
          if (row[field.name]) {
            if (databaseTools.translateField[field.dataTypeID]) {
              returnRow[field.name] = databaseTools.translateField[field.dataTypeID](row[field.name]);
            } else {
              returnRow[field.name] = row[field.name].toString();
            }
          }
        });
        returnValue[type].push(returnRow);
      });

      //      returnValue.test = results;
      return returnValue;
    }
  };

  return {'query': databaseTools.query};
};

<commit_msg>Add time logging for debugging<commit_after>var pg = require('pg');
var config = require('../config');

exports = module.exports = function(req, res) {

  var databaseTools = {
    database: function (callback) {
      var connectionString = [
        'postgres://',
        config.database.username,
        ':',
        config.database.password,
        '@',
        config.database.address,
        '/',config.database.name
      ].join('');
      pg.connect(connectionString, callback);
    },
    addParams: function (query, type) {
      // TODO: This should use handlebars or something like it
      var newQuery = query,
      re = function(name) {return new RegExp('{{'+name+'}}','g');},
      param;
      for (param in req.params) {
        newQuery = newQuery.replace(re(param), req.params[param]);
      }
      newQuery = newQuery.replace(re('&type&'), type);
      //console.log(newQuery);
      return newQuery;
    },
    query: function(query, type, callback) {
      var queryResult = [],
      startTime = new Date();
      console.log('starting ' + type);
      databaseTools.database(function(err, client, done) {
        if (err) {
          queryResult.error = {'code': '500'};
          queryResult.details = err;
          callback(res, queryResult);
        } else {
          client.query(databaseTools.addParams(query, type), function(err, results) {
            if (err) {
              queryResult.error = {'code': '500'};
              queryResult.details = err;
            } else {
              console.log('finished ' + type + ' after: ' + (new Date() - startTime) + 'ms');
              queryResult.data = databaseTools.parse(results, type);
            }
            callback(res, queryResult);
            done();
          });
        }
      });
    },
    translateField: {
      '17987': function (hstore) {
        //hstore
        var returnValue = [];

        hstore.split('", "').map(function (tag) {
          var tags = tag.split('=>');
          returnValue.push({'k': tags[0].replace(/"$|^"/g, ''), 'v': tags[1].replace(/"$|^"/g, '')});
        });

        return returnValue;
      },
      '25846': function(hstore) {return databaseTools.translateField['17987'](hstore);},
      '114': function(json) {
        return json;
      },
      '1184': function (timestamp){
        // timestamp
        return timestamp.toISOString().replace('.000','');
      },
      '1016': function (ints) {
        //Array of ints
        var returnValue = [];

        ints.toString().split(',').map(function (val) {
          returnValue.push({'ref': val});
        });

        return returnValue;
      }
    },
    parse: function(results, type) {
      var returnValue = {}, returnRow;
      returnValue[type] = [];

      results.rows.map(function(row) {
        returnRow = {};
        results.fields.map(function(field){
          if (row[field.name]) {
            if (databaseTools.translateField[field.dataTypeID]) {
              returnRow[field.name] = databaseTools.translateField[field.dataTypeID](row[field.name]);
            } else {
              returnRow[field.name] = row[field.name].toString();
            }
          }
        });
        returnValue[type].push(returnRow);
      });

      //      returnValue.test = results;
      return returnValue;
    }
  };

  return {'query': databaseTools.query};
};

<|endoftext|>


<commit_before>/* @flow */

import async from 'async'
import Promise from 'bluebird'
import EventEmitter from 'events'
import clone from 'lodash.clone'
import fs from 'fs-extra'
import path from 'path'
import * as stream from 'stream'
import trash from 'trash'

import Config from '../config'
import { TMP_DIR_NAME } from './constants'
import logger from '../logger'
import { isUpToDate } from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'
import { hideOnWindows } from '../utils/fs'
import Watcher from './watcher'

import type { FileStreamProvider } from '../file_stream_provider'
import type { Metadata } from '../metadata'
import type { Side } from '../side' // eslint-disable-line
import type { Callback } from '../utils/func'

const log = logger({
  component: 'LocalWriter'
})
// Local is the class that interfaces cozy-desktop with the local filesystem.
// It uses a watcher, based on chokidar, to listen for file and folder changes.
// It also applied changes from the remote cozy on the local filesystem.
class Local implements Side {
  prep: Prep
  pouch: Pouch
  events: EventEmitter
  syncPath: string
  tmpPath: string
  watcher: Watcher
  other: FileStreamProvider

  constructor (config: Config, prep: Prep, pouch: Pouch, events: EventEmitter) {
    this.prep = prep
    this.pouch = pouch
    this.events = events
    this.syncPath = config.syncPath
    this.tmpPath = path.join(this.syncPath, TMP_DIR_NAME)
    this.watcher = new Watcher(this.syncPath, this.prep, this.pouch)
    // $FlowFixMe
    this.other = null

    Promise.promisifyAll(this)
  }

  // Start initial replication + watching changes in live
  start () {
    fs.ensureDirSync(this.syncPath)
    return this.watcher.start()
  }

  // Stop watching the file system
  stop () {
    return this.watcher.stop()
  }

  // Create a readable stream for the given doc
  createReadStreamAsync (doc: Metadata): Promise<stream.Readable> {
    try {
      let filePath = path.resolve(this.syncPath, doc.path)
      let stream = fs.createReadStream(filePath)
      return new Promise((resolve, reject) => {
        stream.on('open', () => resolve(stream))
        stream.on('error', err => reject(err))
      })
    } catch (err) {
      return Promise.reject(err)
    }
  }

  /* Helpers */

  // Return a function that will update last modification date
  // and does a chmod +x if the file is executable
  //
  // Note: UNIX has 3 timestamps for a file/folder:
  // - atime for last access
  // - ctime for change (metadata or content)
  // - utime for update (content only)
  // This function updates utime and ctime according to the last
  // modification date.
  metadataUpdater (doc: Metadata) {
    let filePath = path.resolve(this.syncPath, doc.path)
    return function (callback: Callback) {
      let next = function (err) {
        if (doc.executable) {
          fs.chmod(filePath, '755', callback)
        } else {
          callback(err)
        }
      }
      if (doc.updated_at) {
        let updated = new Date(doc.updated_at)
        fs.utimes(filePath, updated, updated, () =>
          // Ignore errors
          next()
        )
      } else {
        next()
      }
    }
  }

  // Check if a file corresponding to given checksum already exists
  fileExistsLocally (checksum: string, callback: Callback) {
    this.pouch.byChecksum(checksum, (err, docs) => {
      if (err) {
        callback(err)
      } else if ((docs == null) || (docs.length === 0)) {
        callback(null, false)
      } else {
        let paths = Array.from(docs)
          .filter((doc) => isUpToDate('local', doc))
          .map((doc) => path.resolve(this.syncPath, doc.path))
        async.detect(paths,
            (filePath, next) => fs.exists(filePath, found => next(null, found)),
            callback)
      }
    })
  }

  /* Write operations */

  // Add a new file, or replace an existing one
  //
  // Steps to create a file:
  //   * Try to find a similar file based on his checksum
  //     (in that case, it just requires a local copy)
  //   * Or download the linked binary from remote
  //   * Write to a temporary file
  //   * Ensure parent folder exists
  //   * Move the temporay file to its final destination
  //   * Update creation and last modification dates
  //
  // Note: if no checksum was available for this file, we download the file
  // from the remote document. Later, chokidar will fire an event for this new
  // file. The checksum will then be computed and added to the document, and
  // then pushed to CouchDB.
  addFile (doc: Metadata, callback: Callback) {
    let tmpFile = path.resolve(this.tmpPath, `${path.basename(doc.path)}.tmp`)
    let filePath = path.resolve(this.syncPath, doc.path)
    let parent = path.resolve(this.syncPath, path.dirname(doc.path))

    log.info({path: filePath}, 'Put file')

    async.waterfall([
      next => {
        if (doc.md5sum != null) {
          this.fileExistsLocally(doc.md5sum, next)
        } else {
          next(null, false)
        }
      },

      (existingFilePath, next) => {
        fs.ensureDir(this.tmpPath, () => {
          hideOnWindows(this.tmpPath)
          if (existingFilePath) {
            log.info({path: filePath}, `Recopy ${existingFilePath} -> ${filePath}`)
            this.events.emit('transfer-copy', doc)
            fs.copy(existingFilePath, tmpFile, next)
          } else {
            this.other.createReadStreamAsync(doc).then(
              (stream) => {
                // Don't use async callback here!
                // Async does some magic and the stream can throw an
                // 'error' event before the next async is called...
                let target = fs.createWriteStream(tmpFile)
                stream.pipe(target)
                target.on('finish', next)
                target.on('error', next)
                // Emit events to track the download progress
                let info = clone(doc)
                info.way = 'down'
                info.eventName = `transfer-down-${doc._id}`
                this.events.emit('transfer-started', info)
                stream.on('data', data => {
                  this.events.emit(info.eventName, data)
                })
                target.on('finish', () => {
                  this.events.emit(info.eventName, {finished: true})
                })
              },
              (err) => { next(err) }
            )
          }
        })
      },

      next => {
        if (doc.md5sum != null) {
          this.watcher.checksum(tmpFile, function (err, md5sum) {
            if (err) {
              next(err)
            } else if (md5sum === doc.md5sum) {
              next()
            } else {
              next(new Error('Invalid checksum'))
            }
          })
        } else {
          next()
        }
      },

      next => fs.ensureDir(parent, () => fs.rename(tmpFile, filePath, next)),

      this.metadataUpdater(doc)

    ], function (err) {
      if (err) { log.warn({path: doc.path}, 'addFile failed:', err, doc) }
      fs.unlink(tmpFile, () => callback(err))
    })
  }

  addFileAsync: (Metadata) => Promise<*>

  // Create a new folder
  addFolder (doc: Metadata, callback: Callback) {
    let folderPath = path.join(this.syncPath, doc.path)
    log.info({path: folderPath}, 'Put folder')
    fs.ensureDir(folderPath, err => {
      if (err) {
        callback(err)
      } else {
        this.metadataUpdater(doc)(callback)
      }
    })
  }

  addFolderAsync: (Metadata) => Promise<*>

  // Overwrite a file
  overwriteFileAsync (doc: Metadata, old: ?Metadata): Promise<*> {
    return this.addFileAsync(doc)
  }

  // Update the metadata of a file
  updateFileMetadata (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, 'Updating file metadata...')
    this.metadataUpdater(doc)(callback)
  }

  updateFileMetadataAsync: (Metadata, Metadata) => Promise<*>

  // Update a folder
  updateFolderAsync (doc: Metadata, old: Metadata): Promise<*> {
    return this.addFolderAsync(doc)
  }

  // Move a file from one place to another
  moveFile (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, `Moving from ${old.path}`)
    let oldPath = path.join(this.syncPath, old.path)
    let newPath = path.join(this.syncPath, doc.path)
    let parent = path.join(this.syncPath, path.dirname(doc.path))

    async.waterfall([
      next => fs.exists(oldPath, function (oldPathExists) {
        if (oldPathExists) {
          fs.ensureDir(parent, () => fs.rename(oldPath, newPath, next))
        } else {
          fs.exists(newPath, function (newPathExists) {
            if (newPathExists) {
              next()
            } else {
              const msg = `File ${oldPath} not found`
              log.error({path: newPath}, msg)
              next(new Error(msg))
            }
          })
        }
      }),

      this.metadataUpdater(doc)

    ], err => {
      if (err) {
        log.error({path: newPath}, `Error while moving ${JSON.stringify(doc, null, 2)}`)
        log.trace({path: newPath}, JSON.stringify(old, null, 2))
        log.error({path: newPath, err})
        this.addFile(doc, callback)
      } else {
        this.events.emit('transfer-move', doc, old)
        callback(null)
      }
    })
  }

  moveFileAsync: (Metadata, Metadata) => Promise<*>

  // Move a folder
  moveFolder (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, `Move folder from ${old.path}`)
    let oldPath = path.join(this.syncPath, old.path)
    let newPath = path.join(this.syncPath, doc.path)
    let parent = path.join(this.syncPath, path.dirname(doc.path))

    async.waterfall([
      next => fs.exists(oldPath, oldPathExists =>
        fs.exists(newPath, function (newPathExists) {
          if (oldPathExists && newPathExists) {
            fs.rmdir(oldPath, next)
          } else if (oldPathExists) {
            fs.ensureDir(parent, () => fs.rename(oldPath, newPath, next))
          } else if (newPathExists) {
            next()
          } else {
            const msg = `Folder ${oldPath} not found`
            log.error({path: newPath}, msg)
            next(new Error(msg))
          }
        })
      ),

      this.metadataUpdater(doc)

    ], err => {
      if (err) {
        log.error({path: newPath}, `Error while moving ${JSON.stringify(doc, null, 2)}`)
        log.trace({path: newPath}, JSON.stringify(old, null, 2))
        log.error({path: newPath, err})
        this.addFolder(doc, callback)
      } else {
        callback(null)
      }
    })
  }

  moveFolderAsync: (Metadata, Metadata) => Promise<*>

  trashAsync (doc: Metadata): Promise<*> {
    log.info({path: doc.path}, 'Moving to the OS trash...')
    this.events.emit('delete-file', doc)
    let fullpath = path.join(this.syncPath, doc.path)
    return trash([fullpath])
  }

  // Rename a file/folder to resolve a conflict
  resolveConflict (dst: Metadata, src: Metadata, callback: Callback) {
    log.info({path: src.path}, `Resolve a conflict: ${src.path} → ${dst.path}`)
    let srcPath = path.join(this.syncPath, src.path)
    let dstPath = path.join(this.syncPath, dst.path)
    fs.rename(srcPath, dstPath, callback)
    // Don't fire an event for the deleted file
    setTimeout(() => {
      const p = this.watcher.pending
      if (p.hasPath(src.path)) { p.clear(src.path) }
    }, 1000)
  }

  resolveConflictAsync: (Metadata, Metadata) => Promise<*>
}

export default Local
<commit_msg>Introduce Local#_trash property<commit_after>/* @flow */

import async from 'async'
import Promise from 'bluebird'
import EventEmitter from 'events'
import clone from 'lodash.clone'
import fs from 'fs-extra'
import path from 'path'
import * as stream from 'stream'
import trash from 'trash'

import Config from '../config'
import { TMP_DIR_NAME } from './constants'
import logger from '../logger'
import { isUpToDate } from '../metadata'
import Pouch from '../pouch'
import Prep from '../prep'
import { hideOnWindows } from '../utils/fs'
import Watcher from './watcher'

import type { FileStreamProvider } from '../file_stream_provider'
import type { Metadata } from '../metadata'
import type { Side } from '../side' // eslint-disable-line
import type { Callback } from '../utils/func'

const log = logger({
  component: 'LocalWriter'
})
// Local is the class that interfaces cozy-desktop with the local filesystem.
// It uses a watcher, based on chokidar, to listen for file and folder changes.
// It also applied changes from the remote cozy on the local filesystem.
class Local implements Side {
  prep: Prep
  pouch: Pouch
  events: EventEmitter
  syncPath: string
  tmpPath: string
  watcher: Watcher
  other: FileStreamProvider
  _trash: (Array<string>) => Promise

  constructor (config: Config, prep: Prep, pouch: Pouch, events: EventEmitter) {
    this.prep = prep
    this.pouch = pouch
    this.events = events
    this.syncPath = config.syncPath
    this.tmpPath = path.join(this.syncPath, TMP_DIR_NAME)
    this.watcher = new Watcher(this.syncPath, this.prep, this.pouch)
    // $FlowFixMe
    this.other = null
    this._trash = trash

    Promise.promisifyAll(this)
  }

  // Start initial replication + watching changes in live
  start () {
    fs.ensureDirSync(this.syncPath)
    return this.watcher.start()
  }

  // Stop watching the file system
  stop () {
    return this.watcher.stop()
  }

  // Create a readable stream for the given doc
  createReadStreamAsync (doc: Metadata): Promise<stream.Readable> {
    try {
      let filePath = path.resolve(this.syncPath, doc.path)
      let stream = fs.createReadStream(filePath)
      return new Promise((resolve, reject) => {
        stream.on('open', () => resolve(stream))
        stream.on('error', err => reject(err))
      })
    } catch (err) {
      return Promise.reject(err)
    }
  }

  /* Helpers */

  // Return a function that will update last modification date
  // and does a chmod +x if the file is executable
  //
  // Note: UNIX has 3 timestamps for a file/folder:
  // - atime for last access
  // - ctime for change (metadata or content)
  // - utime for update (content only)
  // This function updates utime and ctime according to the last
  // modification date.
  metadataUpdater (doc: Metadata) {
    let filePath = path.resolve(this.syncPath, doc.path)
    return function (callback: Callback) {
      let next = function (err) {
        if (doc.executable) {
          fs.chmod(filePath, '755', callback)
        } else {
          callback(err)
        }
      }
      if (doc.updated_at) {
        let updated = new Date(doc.updated_at)
        fs.utimes(filePath, updated, updated, () =>
          // Ignore errors
          next()
        )
      } else {
        next()
      }
    }
  }

  // Check if a file corresponding to given checksum already exists
  fileExistsLocally (checksum: string, callback: Callback) {
    this.pouch.byChecksum(checksum, (err, docs) => {
      if (err) {
        callback(err)
      } else if ((docs == null) || (docs.length === 0)) {
        callback(null, false)
      } else {
        let paths = Array.from(docs)
          .filter((doc) => isUpToDate('local', doc))
          .map((doc) => path.resolve(this.syncPath, doc.path))
        async.detect(paths,
            (filePath, next) => fs.exists(filePath, found => next(null, found)),
            callback)
      }
    })
  }

  /* Write operations */

  // Add a new file, or replace an existing one
  //
  // Steps to create a file:
  //   * Try to find a similar file based on his checksum
  //     (in that case, it just requires a local copy)
  //   * Or download the linked binary from remote
  //   * Write to a temporary file
  //   * Ensure parent folder exists
  //   * Move the temporay file to its final destination
  //   * Update creation and last modification dates
  //
  // Note: if no checksum was available for this file, we download the file
  // from the remote document. Later, chokidar will fire an event for this new
  // file. The checksum will then be computed and added to the document, and
  // then pushed to CouchDB.
  addFile (doc: Metadata, callback: Callback) {
    let tmpFile = path.resolve(this.tmpPath, `${path.basename(doc.path)}.tmp`)
    let filePath = path.resolve(this.syncPath, doc.path)
    let parent = path.resolve(this.syncPath, path.dirname(doc.path))

    log.info({path: filePath}, 'Put file')

    async.waterfall([
      next => {
        if (doc.md5sum != null) {
          this.fileExistsLocally(doc.md5sum, next)
        } else {
          next(null, false)
        }
      },

      (existingFilePath, next) => {
        fs.ensureDir(this.tmpPath, () => {
          hideOnWindows(this.tmpPath)
          if (existingFilePath) {
            log.info({path: filePath}, `Recopy ${existingFilePath} -> ${filePath}`)
            this.events.emit('transfer-copy', doc)
            fs.copy(existingFilePath, tmpFile, next)
          } else {
            this.other.createReadStreamAsync(doc).then(
              (stream) => {
                // Don't use async callback here!
                // Async does some magic and the stream can throw an
                // 'error' event before the next async is called...
                let target = fs.createWriteStream(tmpFile)
                stream.pipe(target)
                target.on('finish', next)
                target.on('error', next)
                // Emit events to track the download progress
                let info = clone(doc)
                info.way = 'down'
                info.eventName = `transfer-down-${doc._id}`
                this.events.emit('transfer-started', info)
                stream.on('data', data => {
                  this.events.emit(info.eventName, data)
                })
                target.on('finish', () => {
                  this.events.emit(info.eventName, {finished: true})
                })
              },
              (err) => { next(err) }
            )
          }
        })
      },

      next => {
        if (doc.md5sum != null) {
          this.watcher.checksum(tmpFile, function (err, md5sum) {
            if (err) {
              next(err)
            } else if (md5sum === doc.md5sum) {
              next()
            } else {
              next(new Error('Invalid checksum'))
            }
          })
        } else {
          next()
        }
      },

      next => fs.ensureDir(parent, () => fs.rename(tmpFile, filePath, next)),

      this.metadataUpdater(doc)

    ], function (err) {
      if (err) { log.warn({path: doc.path}, 'addFile failed:', err, doc) }
      fs.unlink(tmpFile, () => callback(err))
    })
  }

  addFileAsync: (Metadata) => Promise<*>

  // Create a new folder
  addFolder (doc: Metadata, callback: Callback) {
    let folderPath = path.join(this.syncPath, doc.path)
    log.info({path: folderPath}, 'Put folder')
    fs.ensureDir(folderPath, err => {
      if (err) {
        callback(err)
      } else {
        this.metadataUpdater(doc)(callback)
      }
    })
  }

  addFolderAsync: (Metadata) => Promise<*>

  // Overwrite a file
  overwriteFileAsync (doc: Metadata, old: ?Metadata): Promise<*> {
    return this.addFileAsync(doc)
  }

  // Update the metadata of a file
  updateFileMetadata (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, 'Updating file metadata...')
    this.metadataUpdater(doc)(callback)
  }

  updateFileMetadataAsync: (Metadata, Metadata) => Promise<*>

  // Update a folder
  updateFolderAsync (doc: Metadata, old: Metadata): Promise<*> {
    return this.addFolderAsync(doc)
  }

  // Move a file from one place to another
  moveFile (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, `Moving from ${old.path}`)
    let oldPath = path.join(this.syncPath, old.path)
    let newPath = path.join(this.syncPath, doc.path)
    let parent = path.join(this.syncPath, path.dirname(doc.path))

    async.waterfall([
      next => fs.exists(oldPath, function (oldPathExists) {
        if (oldPathExists) {
          fs.ensureDir(parent, () => fs.rename(oldPath, newPath, next))
        } else {
          fs.exists(newPath, function (newPathExists) {
            if (newPathExists) {
              next()
            } else {
              const msg = `File ${oldPath} not found`
              log.error({path: newPath}, msg)
              next(new Error(msg))
            }
          })
        }
      }),

      this.metadataUpdater(doc)

    ], err => {
      if (err) {
        log.error({path: newPath}, `Error while moving ${JSON.stringify(doc, null, 2)}`)
        log.trace({path: newPath}, JSON.stringify(old, null, 2))
        log.error({path: newPath, err})
        this.addFile(doc, callback)
      } else {
        this.events.emit('transfer-move', doc, old)
        callback(null)
      }
    })
  }

  moveFileAsync: (Metadata, Metadata) => Promise<*>

  // Move a folder
  moveFolder (doc: Metadata, old: Metadata, callback: Callback) {
    log.info({path: doc.path}, `Move folder from ${old.path}`)
    let oldPath = path.join(this.syncPath, old.path)
    let newPath = path.join(this.syncPath, doc.path)
    let parent = path.join(this.syncPath, path.dirname(doc.path))

    async.waterfall([
      next => fs.exists(oldPath, oldPathExists =>
        fs.exists(newPath, function (newPathExists) {
          if (oldPathExists && newPathExists) {
            fs.rmdir(oldPath, next)
          } else if (oldPathExists) {
            fs.ensureDir(parent, () => fs.rename(oldPath, newPath, next))
          } else if (newPathExists) {
            next()
          } else {
            const msg = `Folder ${oldPath} not found`
            log.error({path: newPath}, msg)
            next(new Error(msg))
          }
        })
      ),

      this.metadataUpdater(doc)

    ], err => {
      if (err) {
        log.error({path: newPath}, `Error while moving ${JSON.stringify(doc, null, 2)}`)
        log.trace({path: newPath}, JSON.stringify(old, null, 2))
        log.error({path: newPath, err})
        this.addFolder(doc, callback)
      } else {
        callback(null)
      }
    })
  }

  moveFolderAsync: (Metadata, Metadata) => Promise<*>

  trashAsync (doc: Metadata): Promise<*> {
    log.info({path: doc.path}, 'Moving to the OS trash...')
    this.events.emit('delete-file', doc)
    let fullpath = path.join(this.syncPath, doc.path)
    return this._trash([fullpath])
  }

  // Rename a file/folder to resolve a conflict
  resolveConflict (dst: Metadata, src: Metadata, callback: Callback) {
    log.info({path: src.path}, `Resolve a conflict: ${src.path} → ${dst.path}`)
    let srcPath = path.join(this.syncPath, src.path)
    let dstPath = path.join(this.syncPath, dst.path)
    fs.rename(srcPath, dstPath, callback)
    // Don't fire an event for the deleted file
    setTimeout(() => {
      const p = this.watcher.pending
      if (p.hasPath(src.path)) { p.clear(src.path) }
    }, 1000)
  }

  resolveConflictAsync: (Metadata, Metadata) => Promise<*>
}

export default Local
<|endoftext|>