<commit_before>$(function(){

  var JOBS = 3;

  // Cache selects
  var select_presets = $("select.encoders");

  var getPresets = function() {

    var presets = {};

    // Clear the select elements
    select_presets.html("");

    var sources = [];

    // Get preset data from ~/.encloader/presets.js
    var presetFile = Titanium.Filesystem.getFile(
      Titanium.Filesystem.getUserDirectory(), ".encloader", "presets.js"
    );
    if (presetFile.isFile()) {
      var stream = Titanium.Filesystem.getFileStream(presetFile);
      if (stream.open()) {
        var data = stream.read(10000);
        data = $.parseJSON($.trim(data));
        sources = data;
      }
      stream.close();
    }

    sources.unshift({
      "name": "Built-in",
      "presets": [
        {
          "id": "e8U1qZ",
          "type": "ENC",
          "name": "H.264 720p",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-crf", "25", "-flags", "+loop+mv4", "-cmp", "256", "-partitions", "+parti4x4+parti8x8+partp4x4+partp8x8+partb8x8", "-me_method", "hex", "-subq", "7", "-trellis", "1", "-refs", "5", "-bf", "0", "-flags2", "+mixed_refs", "-coder", "0", "-me_range", "16", "-g", "250", "-keyint_min", "25", "-sc_threshold", "40", "-i_qfactor", "0.71", "-qmin", "10", "-qmax", "51", "-acodec", "aac", "-b:a", "256k", "-ac", "2", "-r:a", "48k", "-strict", "experimental", "-vf", 'yadif,scale=1280:720', "-threads", "{{threads}}", "{{outfile}}-720p.mp4"],
            ["qtfaststart.py", "{{outfile}}-720p.mp4"]
          ]
        },
        {
          "id": "k8Ea25",
          "type": "ENC",
          "name": "H.264 Mobile",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-crf", "25", "-flags", "+loop+mv4", "-cmp", "256", "-partitions", "+parti4x4+parti8x8+partp4x4+partp8x8+partb8x8", "-me_method", "hex", "-subq", "7", "-trellis", "1", "-refs", "5", "-bf", "0", "-flags2", "+mixed_refs", "-coder", "0", "-me_range", "16", "-g", "250", "-keyint_min", "25", "-sc_threshold", "40", "-i_qfactor", "0.71", "-qmin", "10", "-qmax", "51", "-acodec", "aac", "-b:a", "192k", "-ac", "2", "-r:a", "48k", "-strict", "experimental", "-vf", 'yadif,scale=640:360', "-threads", "{{threads}}", "{{outfile}}-mobile.mp4"],
            ["qtfaststart.py", "{{outfile}}-mobile.mp4"]
          ]
        },
        {
          "id": "8wI2L9",
          "type": "ENC",
          "name": "Animation",
          "cmd": ["ffmpeg", "-i", "{{infile}}", "-vcodec", "qtrle", "-g", "1", "-acodec", "pcm_s16le", "{{outfile}}-animation.mov"
          ]
        },
        {
          "id": "zIs82L",
          "type": "ENC",
          "name": "5D to DNx175 MXF",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-vcodec", "dnxhd", "-b:v", "175M", "-pix_fmt", "yuv422p", "-s", "1920x1080", "-acodec", "pcm_s16le", "-ar", "48000", "-ac", "2", "-threads", "{{threads}}", "-vf", "crop=in_w:in_h-8:0:0,scale=1920:1080", "{{tempfile}}.mov"],
            ["ffmpeg", "-i", "{{tempfile}}.mov", "-an", "-vcodec", "copy", "{{tempfile}}.m2v"],
            ["ffmpeg", "-i", "{{tempfile}}.mov", "-vn", "-acodec", "pcm_s16le", "{{tempfile}}.wav"],
            ["writeavidmxf", "--prefix", "{{outfile}}", "--film23.976", "--DNxHD1080p175", "{{tempfile}}.m2v", "--wavpcm", "{{tempfile}}.wav"]
          ],
        }
      ]
    });

    // Flatten sources
    var addSource = function(source) {
      _(source.presets).each(function(preset, i) {
        preset["source"] = source.name;
        preset["order"] = i;
        select_presets.append(html.option(
          preset.id,
          preset.name
        ));
        presets[preset.id] = preset;
      });
    };
    _(sources).each(function(source) {
      if (_.isUndefined(source.name)) {
        $.getJSON(source.url, function(data) {
          _(source).extend(data);
          addSource(source);
        });
      }
      else {
        addSource(source);
      }
    });

    // Return a flat object of all the presets
    return presets;

  };

  // Retrieve presets and render selects
  window.Presets = getPresets();

  var JobQueue = Class.$extend({

    __init__: function() {
      this.queue = [];
      this.finished = 0;
      this.working = 0;
      this.max_working = 3;
    },

    addJob: function(job) {
      this.queue.push(job);
      if (this.working < this.max_working) {
        job.launch();
        this.working++;
      }
    }

  });
  var jobq = JobQueue([]);

  var JobView = Class.$extend({

    __init__: function(job) {

      this.job = job;

      this.percent = 0;
      this.state = "Pending...";

      this.el = $(document.createElement("div"));
      this.el.html(this.template({
        title: ospath.basename(this.job.infile),
        state: this.state,
        percent: this.percent
      }));

      this.el_state = this.el.find("p.state");
      this.el_progress = this.el.find("div.progress-bar div");
      this.el_output = this.el.find("div.output");
      this.el_kill = this.el.find("a.close");

      var jobview = this;
      this.el_kill.click(function() {
        publish("/job/killed", [jobview.job]);
      });

      $("div.jobs").prepend(this.el);

    },

    template: _.template($("#job-template").html()),

    set: function(attr, val) {
      this[attr] = val;
    },

    setPercent: function(val) {
      this.percent = val;
      this.el_progress.css({width: this.percent+"%"});
      if (this.percent == 100) {
        this.setState("Done.");
      }
    },

    setState: function(val) {
      this.state = val;
      this.el_state.html(this.state);
    },

    startUnknown: function() {
      this.setPercent(0);
    },

    endUnknown: function() {
      this.setPercent(100);
    },

    addOutput: function(line) {
      this.el_output.append(line + "
");
      this.el_output[0].scrollTop = this.el_output[0].scrollHeight;
    }

  });

  var LinkedList = Class.$extend({

    __init__: function() {
      this.length = 0;
      this.first = null;
      this.last = null;
    },

    append: function(node) {
      node.next = null;
      if (this.first === null) {
        this.first = node;
        this.last = node;
      }
      else {
        this.last.next = node;
        this.last = node;
      }
      this.length++;
    }

  });

  var ProcessBase = Class.$extend({

    __init__: function(cmd, job) {
      this.job = job; // todo: change el to job.view
      this.process = Titanium.Process.createProcess(cmd, {
        "PATH": binpath.toString() + ":/usr/bin:/bin"
      });
      this.el = job.view;
      var p = this;
      this.process.setOnReadLine(function(data){
        p.el.addOutput(data.toString());
      });
      this.process.setOnExit(function() {
        p.el.setPercent(100);
        publish("/process/finished", [p]);
      });
    },

    launch: function() {
      this.el.addOutput(this.process.toString());
      this.process.launch();
      this.el.setState("Running...");
    },

    kill: function() {
      this.process.kill();
      this.el.setState("Canceled.");
    }

  });

  var FFmpegProcess = ProcessBase.$extend({

    __init__: function(cmd, job) {

      this.$super(cmd, job);

      var p = this;
      var duration_re = /Duration: (\d\d):(\d\d):(\d\d)\.(\d\d)/g;
      var duration = 0;
      var time_re = /time=(\d\d):(\d\d):(\d\d)\.(\d\d)/g;
      var tcTupleToSeconds = function(tc) {
        return tc[0]*60*60 + tc[1]*60 + tc[2]*1 + tc[3]*.01;
      };
      this.process.setOnReadLine(function(data){
        var line = data.toString();
        p.el.addOutput(line);
        if (!duration) {
          var match = duration_re.exec(line);
          if (match) {
            duration = tcTupleToSeconds(match.slice(1));
          }
        }
        else {
          var match = time_re.exec(line);
          if (match) {
            var secs = tcTupleToSeconds(match.slice(1));
            p.el.setPercent(secs / duration * 100);
          }
        }
      });

    }

  });
  // todo: register this process type

  // Returns Titanium.Filesystem.File outfile
  var getOutputFile = function(infile, xtrapath) {

    var outfile = [Titanium.Filesystem.getDesktopDirectory().toString()];

    if (xtrapath) {
      outfile.push(xtrapath);
    }

    // If no basename is specified, use the input file basename.
    if ((!xtrapath) || (xtrapath.slice(-1) == '/')) {
      // Remove the file extension.
      infile = infile.slice(0, infile.lastIndexOf("."));
      outfile.push(ospath.basename(infile));
    }

    outfile = Titanium.Filesystem.getFile(outfile);
    return outfile;

  };

  var mapTemplate = function(list, context) {
    // Substitute placeholders in command arguments
    return _.map(list, function(item) {
      return _.template(item)(context);
    });
  }

  var NewJob = LinkedList.$extend({

    __init__: function(infile, encoder_id, xtrapath) {

      // A job is a list of processes
      this.$super();

      this.infile = infile;

      this.view = JobView(this);

      var cmds = Presets[encoder_id].cmd;

      // Killed flag
      this.killed = 0;

      // Incase there is only one command, make a list of one command anyway.
      if (typeof(cmds[0]) === "string") {
        cmds = [cmds];
      }

      // Create argument subsitution variables.
      this.tempdir = Titanium.Filesystem.getApplicationDataDirectory();
      this.tempbase = random.string(6);
      subs = {
        infile: infile,
        outfile: getOutputFile(infile, xtrapath).toString(),
        threads: Titanium.Platform.getProcessorCount() + "",
        tempfile: Titanium.Filesystem.getFile(this.tempdir, this.tempbase)
      };

      // Get the outfiles and tempfiles
      this.outfiles = [];
      this.tempfiles = [];
      _.each(_.flatten(cmds), function(arg) {
        // todo: make the actual subsitutions here as well
        if (arg.match(/{{outfile}}/)) {
          this.outfiles.push(_.template(arg)(subs));
        }
        if (arg.match(/{{tempfile}}/)) {
          this.tempfiles.push(_.template(arg)(subs));
        }
      }, this);
      this.outfiles = _.uniq(this.outfiles);
      this.tempfiles = _.uniq(this.tempfiles);

      // Make sure these files don't already exist.
      _.each(this.outfiles.concat(this.tempfiles), function(file) {
        file = Titanium.Filesystem.getFile(file);
        if (file.exists()) {
          this.view.setState("File already exists: " + file.toString());
          this.killed = 1;
        }
        // todo: check against other files to be created during the batch.
      }, this);

      // Subsitute the arguments.
      cmds = _.map(cmds, function(cmd) {
        return mapTemplate(cmd, subs);
      });

      // Add each process to this job
      for (var i=0; i<cmds.length; i++) {
        var process = FFmpegProcess(cmds[i], this);
        this.append(process);
      }

      // There is no currently running process
      this.current = null;

    },

    launch: function() {
      this.current = this.first;
      this.first.launch();
    }

  });

  subscribe("/job/killed", function(job) {
    job.killed = 1;
    job.view.setState("Cancelled.");
    job.current.kill();
  });

  subscribe("/job/finished", function(jq) {
    jq.finished++;
    jq.working--;
    var i = jq.finished + jq.working;
    if (jq.queue.length > i) {
      jq.queue[i].launch();
      jq.working++;
    }
  });

  subscribe("/process/finished", function(process) {

    // If there is another process to run... launch it.
    if ((process.next) && (!process.job.killed)) {
      process.current = process.next;
      process.next.launch();
    }

    else {

      // There are no more currently running processes.
      process.current = null;

      // Remove tempfiles
      _.each(process.job.tempfiles, function(file) {
        file.deleteFile();
      });

      // Let the job queue know this finished
      publish("/job/finished", [jobq]);

    }
  });

  // Wait for files to be dropped on the dropzone
  var initDropzone = function(dropzone) {

    window.setInterval(function() {
      var files = dropzone.val();
      if (files) {

        // Clear the dropzone value for the next batch of files to be dropped.
        dropzone.val("");

        // Get the currently selected form values.
        var encoder_id = select_presets.val();

        // Removed the path input for interface simplification.
        // Perhaps it will be re-enabled later or made optional in settings.
        //var path = $("#path").val();
        var path = "";

        // Create a job for each file that was dropped.
        _(files.split("\n")).each(function(file) {
          //var j = Job(file, encoder_id, uploader_id, path);
          var j = NewJob(file, encoder_id, path);
          log.debug(j.killed);
          if (!j.killed) {
            jobq.addJob(j);
          }
        });

      }
    }, 500);

  };
  initDropzone($("textarea.dropzone"));

});
<commit_msg>added prores and proreshq encoding!<commit_after>$(function(){

  var JOBS = 3;

  // Cache selects
  var select_presets = $("select.encoders"); // browser

  var getPresets = function() {

    var presets = {};

    // Clear the select elements
    select_presets.html("");

    var sources = [];

    // Get preset data from ~/.encloader/presets.js
    var presetFile = Titanium.Filesystem.getFile(
      Titanium.Filesystem.getUserDirectory(), ".encloader", "presets.js"
    );
    if (presetFile.isFile()) {
      var stream = Titanium.Filesystem.getFileStream(presetFile);
      if (stream.open()) {
        var data = stream.read(10000);
        data = $.parseJSON($.trim(data));
        sources = data;
      }
      stream.close();
    }

    sources.unshift({
      "name": "Built-in",
      "presets": [
        {
          "id": "e8U1qZ",
          "type": "ENC",
          "name": "H.264 720p",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-crf", "25", "-flags", "+loop+mv4", "-cmp", "256", "-partitions", "+parti4x4+parti8x8+partp4x4+partp8x8+partb8x8", "-me_method", "hex", "-subq", "7", "-trellis", "1", "-refs", "5", "-bf", "0", "-flags2", "+mixed_refs", "-coder", "0", "-me_range", "16", "-g", "250", "-keyint_min", "25", "-sc_threshold", "40", "-i_qfactor", "0.71", "-qmin", "10", "-qmax", "51", "-acodec", "aac", "-b:a", "256k", "-ac", "2", "-r:a", "48k", "-strict", "experimental", "-vf", 'yadif,scale=1280:720', "-threads", "{{threads}}", "{{outfile}}-720p.mp4"],
            ["qtfaststart.py", "{{outfile}}-720p.mp4"]
          ]
        },
        {
          "id": "k8Ea25",
          "type": "ENC",
          "name": "H.264 Mobile",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-crf", "25", "-flags", "+loop+mv4", "-cmp", "256", "-partitions", "+parti4x4+parti8x8+partp4x4+partp8x8+partb8x8", "-me_method", "hex", "-subq", "7", "-trellis", "1", "-refs", "5", "-bf", "0", "-flags2", "+mixed_refs", "-coder", "0", "-me_range", "16", "-g", "250", "-keyint_min", "25", "-sc_threshold", "40", "-i_qfactor", "0.71", "-qmin", "10", "-qmax", "51", "-acodec", "aac", "-b:a", "192k", "-ac", "2", "-r:a", "48k", "-strict", "experimental", "-vf", 'yadif,scale=640:360', "-threads", "{{threads}}", "{{outfile}}-mobile.mp4"],
            ["qtfaststart.py", "{{outfile}}-mobile.mp4"]
          ]
        },
        {
          "id": "8wI2L9",
          "type": "ENC",
          "name": "Animation",
          "cmd": ["ffmpeg", "-i", "{{infile}}", "-vcodec", "qtrle", "-g", "1", "-acodec", "pcm_s16le", "{{outfile}}-animation.mov"
          ]
        },
        {
          "id": "EVLijw",
          "type": "ENC",
          "name": "ProRes",
          "cmd": ["ffmpeg", "-i", "{{infile}}", "-vcodec", "prores", "-profile", "2", "-acodec", "pcm_s16le", "{{outfile}}-prores.mov"
          ]
        },
        {
          "id": "8Z4XWc",
          "type": "ENC",
          "name": "ProRes HQ",
          "cmd": ["ffmpeg", "-i", "{{infile}}", "-vcodec", "prores", "-profile", "3", "-acodec", "pcm_s16le", "{{outfile}}-proreshq.mov"
          ]
        },
        {
          "id": "zIs82L",
          "type": "ENC",
          "name": "5D to DNx175 MXF",
          "cmd": [
            ["ffmpeg", "-i", "{{infile}}", "-vcodec", "dnxhd", "-b:v", "175M", "-pix_fmt", "yuv422p", "-s", "1920x1080", "-acodec", "pcm_s16le", "-ar", "48000", "-ac", "2", "-threads", "{{threads}}", "-vf", "crop=in_w:in_h-8:0:0,scale=1920:1080", "{{tempfile}}.mov"],
            ["ffmpeg", "-i", "{{tempfile}}.mov", "-an", "-vcodec", "copy", "{{tempfile}}.m2v"],
            ["ffmpeg", "-i", "{{tempfile}}.mov", "-vn", "-acodec", "pcm_s16le", "{{tempfile}}.wav"],
            ["writeavidmxf", "--prefix", "{{outfile}}", "--film23.976", "--DNxHD1080p175", "{{tempfile}}.m2v", "--wavpcm", "{{tempfile}}.wav"]
          ],
        }
      ]
    });

    // Flatten sources
    var addSource = function(source) {
      _(source.presets).each(function(preset, i) {
        preset["source"] = source.name;
        preset["order"] = i;
        select_presets.append(html.option(
          preset.id,
          preset.name
        ));
        presets[preset.id] = preset;
      });
    };
    _(sources).each(function(source) {
      if (_.isUndefined(source.name)) {
        $.getJSON(source.url, function(data) {
          _(source).extend(data);
          addSource(source);
        });
      }
      else {
        addSource(source);
      }
    });

    // Return a flat object of all the presets
    return presets;

  };

  // Retrieve presets and render selects
  window.Presets = getPresets(); // browser

  var JobQueue = Class.$extend({

    __init__: function() {
      this.queue = [];
      this.finished = 0;
      this.working = 0;
      this.max_working = 3;
    },

    addJob: function(job) {
      this.queue.push(job);
      if (this.working < this.max_working) {
        job.launch();
        this.working++;
      }
    }

  });
  var jobq = JobQueue([]);

  // browser
  var JobView = Class.$extend({

    __init__: function(job) {

      this.job = job;

      this.percent = 0;
      this.state = "Pending...";

      this.el = $(document.createElement("div"));
      this.el.html(this.template({
        title: ospath.basename(this.job.infile),
        state: this.state,
        percent: this.percent
      }));

      this.el_state = this.el.find("p.state");
      this.el_progress = this.el.find("div.progress-bar div");
      this.el_output = this.el.find("div.output");
      this.el_kill = this.el.find("a.close");

      var jobview = this;
      this.el_kill.click(function() {
        publish("/job/killed", [jobview.job]);
      });

      $("div.jobs").prepend(this.el);

    },

    template: _.template($("#job-template").html()),

    set: function(attr, val) {
      this[attr] = val;
    },

    setPercent: function(val) {
      this.percent = val;
      this.el_progress.css({width: this.percent+"%"});
      if (this.percent == 100) {
        this.setState("Done.");
      }
    },

    setState: function(val) {
      this.state = val;
      this.el_state.html(this.state);
    },

    startUnknown: function() {
      this.setPercent(0);
    },

    endUnknown: function() {
      this.setPercent(100);
    },

    addOutput: function(line) {
      this.el_output.append(line + "
");
      this.el_output[0].scrollTop = this.el_output[0].scrollHeight;
    }

  });

  var LinkedList = Class.$extend({

    __init__: function() {
      this.length = 0;
      this.first = null;
      this.last = null;
    },

    append: function(node) {
      node.next = null;
      if (this.first === null) {
        this.first = node;
        this.last = node;
      }
      else {
        this.last.next = node;
        this.last = node;
      }
      this.length++;
    }

  });

  var ProcessBase = Class.$extend({

    __init__: function(cmd, job) {
      this.job = job; // todo: change el to job.view
      this.process = Titanium.Process.createProcess(cmd, {
        "PATH": binpath.toString() + ":/usr/bin:/bin"
      });
      this.el = job.view;
      var p = this;
      this.process.setOnReadLine(function(data){
        p.el.addOutput(data.toString());
      });
      this.process.setOnExit(function() {
        p.el.setPercent(100);
        publish("/process/finished", [p]);
      });
    },

    launch: function() {
      this.el.addOutput(this.process.toString());
      this.process.launch();
      this.el.setState("Running...");
    },

    kill: function() {
      this.process.kill();
      this.el.setState("Canceled.");
    }

  });

  var FFmpegProcess = ProcessBase.$extend({

    __init__: function(cmd, job) {

      this.$super(cmd, job);

      var p = this;
      var duration_re = /Duration: (\d\d):(\d\d):(\d\d)\.(\d\d)/g;
      var duration = 0;
      var time_re = /time=(\d\d):(\d\d):(\d\d)\.(\d\d)/g;
      var tcTupleToSeconds = function(tc) {
        return tc[0]*60*60 + tc[1]*60 + tc[2]*1 + tc[3]*.01;
      };
      this.process.setOnReadLine(function(data){
        var line = data.toString();
        p.el.addOutput(line);
        if (!duration) {
          var match = duration_re.exec(line);
          if (match) {
            duration = tcTupleToSeconds(match.slice(1));
          }
        }
        else {
          var match = time_re.exec(line);
          if (match) {
            var secs = tcTupleToSeconds(match.slice(1));
            p.el.setPercent(secs / duration * 100);
          }
        }
      });

    }

  });
  // todo: register this process type

  // Returns Titanium.Filesystem.File outfile
  var getOutputFile = function(infile, xtrapath) {

    var outfile = [Titanium.Filesystem.getDesktopDirectory().toString()];

    if (xtrapath) {
      outfile.push(xtrapath);
    }

    // If no basename is specified, use the input file basename.
    if ((!xtrapath) || (xtrapath.slice(-1) == '/')) {
      // Remove the file extension.
      infile = infile.slice(0, infile.lastIndexOf("."));
      outfile.push(ospath.basename(infile));
    }

    outfile = Titanium.Filesystem.getFile(outfile);
    return outfile;

  };

  var mapTemplate = function(list, context) {
    // Substitute placeholders in command arguments
    return _.map(list, function(item) {
      return _.template(item)(context);
    });
  }

  var NewJob = LinkedList.$extend({

    __init__: function(infile, encoder_id, xtrapath) {

      // A job is a list of processes
      this.$super();

      this.infile = infile;

      this.view = JobView(this);

      var cmds = Presets[encoder_id].cmd;

      // Killed flag
      this.killed = 0;

      // Incase there is only one command, make a list of one command anyway.
      if (typeof(cmds[0]) === "string") {
        cmds = [cmds];
      }

      // Create argument subsitution variables.
      this.tempdir = Titanium.Filesystem.getApplicationDataDirectory();
      this.tempbase = random.string(6);
      subs = {
        infile: infile,
        outfile: getOutputFile(infile, xtrapath).toString(),
        threads: Titanium.Platform.getProcessorCount() + "",
        tempfile: Titanium.Filesystem.getFile(this.tempdir, this.tempbase)
      };

      // Get the outfiles and tempfiles
      this.outfiles = [];
      this.tempfiles = [];
      _.each(_.flatten(cmds), function(arg) {
        // todo: make the actual subsitutions here as well
        if (arg.match(/{{outfile}}/)) {
          this.outfiles.push(_.template(arg)(subs));
        }
        if (arg.match(/{{tempfile}}/)) {
          this.tempfiles.push(_.template(arg)(subs));
        }
      }, this);
      this.outfiles = _.uniq(this.outfiles);
      this.tempfiles = _.uniq(this.tempfiles);

      // Make sure these files don't already exist.
      _.each(this.outfiles.concat(this.tempfiles), function(file) {
        file = Titanium.Filesystem.getFile(file);
        if (file.exists()) {
          this.view.setState("File already exists: " + file.toString());
          this.killed = 1;
        }
        // todo: check against other files to be created during the batch.
      }, this);

      // Subsitute the arguments.
      cmds = _.map(cmds, function(cmd) {
        return mapTemplate(cmd, subs);
      });

      // Add each process to this job
      for (var i=0; i<cmds.length; i++) {
        var process = FFmpegProcess(cmds[i], this);
        this.append(process);
      }

      // There is no currently running process
      this.current = null;

    },

    launch: function() {
      this.current = this.first;
      this.first.launch();
    }

  });

  subscribe("/job/killed", function(job) {
    job.killed = 1;
    job.view.setState("Cancelled.");
    job.current.kill();
  });

  subscribe("/job/finished", function(jq) {
    jq.finished++;
    jq.working--;
    var i = jq.finished + jq.working;
    if (jq.queue.length > i) {
      jq.queue[i].launch();
      jq.working++;
    }
  });

  subscribe("/process/finished", function(process) {

    // If there is another process to run... launch it.
    if ((process.next) && (!process.job.killed)) {
      process.current = process.next;
      process.next.launch();
    }

    else {

      // There are no more currently running processes.
      process.current = null;

      // Remove tempfiles
      _.each(process.job.tempfiles, function(file) {
        file.deleteFile();
      });

      // Let the job queue know this finished
      publish("/job/finished", [jobq]);

    }
  });

  // Wait for files to be dropped on the dropzone
  // browser
  var initDropzone = function(dropzone) {

    window.setInterval(function() {
      var files = dropzone.val();
      if (files) {

        // Clear the dropzone value for the next batch of files to be dropped.
        dropzone.val("");

        // Get the currently selected form values.
        var encoder_id = select_presets.val();

        // Removed the path input for interface simplification.
        // Perhaps it will be re-enabled later or made optional in settings.
        //var path = $("#path").val();
        var path = "";

        // Create a job for each file that was dropped.
        _(files.split("\n")).each(function(file) {
          //var j = Job(file, encoder_id, uploader_id, path);
          var j = NewJob(file, encoder_id, path);
          log.debug(j.killed);
          if (!j.killed) {
            jobq.addJob(j);
          }
        });

      }
    }, 500);

  };
  initDropzone($("textarea.dropzone"));

});
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';

const program = require('commander');
const chalk = require('chalk');
const miteApi = require('mite-api');

const pkg = require('./../package.json');
const config = require('./config');
const formater = require('./lib/formater');
const DataOutput = require('./lib/data-output');
const listCommand = require('./lib/commands/list');
const columnOptions = require('./lib/options/columns');
const commandOptions = require('./lib/options');
const { handleError } = require('./lib/errors');

// no other options or comamnd line arguments used, default "period" to "today"
if (process.argv.length === 2) {
  process.argv.splice(2, 0, 'today');
}

program
  .version(pkg.version)
  .description('list time entries', {
    period:
      `name of the period for which the time entries should be shown \
or a single date. F.e. "today" or "last_week or "2019-03-17". \
Defaults to "today" when no other options are used and to "all" \
when other options are used.`
  })
  .arguments('<period>')
  .option.apply(program, commandOptions.toArgs(commandOptions.billable),
    'show entries which are billable or not billable'
  )
  .option(
    '--columns <columns>',
    columnOptions.description(listCommand.columns.options),
    columnOptions.parse
  )
  .option(
    '--customer-id <id>',
    'customer id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option.apply(program, commandOptions.toArgs(commandOptions.format, null, config.get('outputFormat')))
  .option(
    '--from <period|YYYY-MM-DD>',
    'in combination with "to" used for selecting a specific time frame of ' +
    'time entries to return, same as "period" argument or a specific date in ' +
    'the format "YYYY-MM-DD". The period argument is ignored.'
  )
  .option(
    '--group-by <column>',
    'optional grouping parameter which should be used. Valid values are ' +
    listCommand.groupBy.options.join(', ')
  )
  .option(
    '-l, --limit <limit>',
    'optional number of items to show, default is infinite',
    undefined,
    ((val) => parseInt(val, 10))
  )
  .option(
    '--locked <true|false>',
    'filter entries by their locked status',
    (val => ['true', 'yes', 'ja', 'ok', '1'].indexOf(val.toLowerCase()) > -1),
    null
  )
  .option(
    '--min-duration <minDuration>',
    'filter out entries which have a duration below the given value (client side)',
  )
  .option(
    '--max-duration <maxDuration>',
    'filter out entries which have a duration above the given value (client side)',
  )
  .option(
    '--project-id <id>',
    'project id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option(
    '--to <period|YYYY-MM-DD>',
    'in combination with "from" used for selecting a specific time frame of ' +
    'time entries to return, same as "period" argument or a specific date in ' +
    'the format "YYYY-MM-DD". The period argument is ignored.'
  )
  .option.apply(program, commandOptions.toArgs(commandOptions.tracking))
  .option(
    '--reversed',
    'reverse the sorting direction',
  )
  .option(
    '-s --search <query>',
    'search within the notes, to filter by multiple criteria connected with OR use comma-separated query values',
    ((val) => {
      if (typeof val === 'string') {
        return val.split(/\s*,\s*/);
      }
      return val;
    })
  )
  .option(
    '--service-id <id>',
    'service id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option.apply(program, commandOptions.toArgs(
    commandOptions.sort,
    commandOptions.sort.description(listCommand.sort.options),
    listCommand.sort.default
  ))
  .option(
    '--user-id <id>',
    'optional single user id who’s time entries should be returned or ' +
    'multiple values comma-separated. Note that the current user may not ' +
    'have the permission ot read other user’s time entries which will result ' +
    'in an empty response',
    ((val) => {
      if (typeof val === 'string') {
        return val.split(/\s*,\s*/);
      }
      return val;
    })
  )
  .on('--help', function() {
    console.log(`
Examples:

  list all entries from the current month:
    mite list this_month

  list all entries which note contains the given search query:
    mite list this_year --search JIRA-123

  show all entries from two services 123 and 38171
    mite list last_month --service-id 123,38171

  show all users who tracked billable entries ordered by the amount of time
  they have tracked:
    mite list this_year --billable true --columns user,duration --group-by user --sort duration

  export all time-entries from the current month as csv:
    mite list last_week --format csv --columns user,id

  create a markdown report of all customer and their generated profits:
    mite list this_year --format md --group-by customer --sort revenue

  The output of mite list can be forwarded to other commands using xargs. The
  following example will delete all matchin entries:
    mite list this_month --search="query" --columns id --format text | xargs -n1 mite delete

  Get the notes for one specific service, project for the last month to put
  them on a bill or similar
    mite list last_month --project-id 2681601 --service-id 325329 --columns note --format text | sort -u

  Show a seperate report for all users showing the revenues and times per
  service for all users matching a query:
    mite users --search marc --columns id --format text | xargs mite list last_month --group-by service --user-id

`);
  })
  .parse(process.argv);

/**
 * Returns the request options for requesting the time entries or grouped
 * time entries for the given time entries and filtering options.
 *
 * @param {string} period
 * @param {object} program
 * @return {Object<String, any>} time entries or grouped time entries
 */
function getRequestOptions(period, program) {

  // mite’s "periods" strings using underscore like in "this_month", "this_week"
  // but sometimes the user enters a minus instead of underscore, then replace
  // it
  if (typeof period === 'string') {
    period = period.replace(/-/g, '_');
  }

  const data = {
    ...(typeof program.billable === 'boolean' && { billable: program.billable }),
    ...(program.customerId && { customer_id: program.customerId }),
    ...(program.reversed && { direction: 'asc' }),
    ...(program.groupBy && { group_by: program.groupBy }),
    ...(program.limit && { limit: program.limit }),
    ...(typeof program.locked === 'boolean' && { locked: program.locked }),
    ...(program.search && { note: program.search }),
    ...(program.projectId && { project_id: program.projectId }),
    ...(program.serviceId && { service_id: program.serviceId }),
    ...(program.sort && { sort: program.sort }),
    ...(typeof program.tracking === 'boolean' && { tracking: program.tracking }),
    ...(program.userId && { user_id: program.userId}),
  };

  // add optional time period using from & to
  if (program.from && program.to) {
    data.from = program.from;
    data.to = program.to;
  } else if (period) {
    data.at = period;
  }

  return data;
}

/**
 * Callback function to filter time entries which are shown depending on the
 * command line options
 *
 * @param {object} program
 * @param {MiteTimeEntry} entry
 * @return {boolean}
 */
function filterData(program, row) {
  let valid = true;
  if (program.minDuration) {
    const minDuration = formater.durationToMinutes(program.minDuration, 10);
    valid &= row.minutes >= minDuration;
  }
  if (program.maxDuration) {
    const maxDuration = formater.durationToMinutes(program.maxDuration, 10);
    valid &= row.minutes <= maxDuration;
  }
  return valid;
}

function getReport(items, columns, format) {
  const tableData = DataOutput.compileTableData(items, columns, format);

  // Table footer
  // add table footer if any of the table columns has a reducer
  if (columnOptions.hasReducer(columns)) {
    let footerColumns = DataOutput.getTableFooterColumns(items, columns);
    footerColumns = footerColumns.map(v => v ? chalk.bold(v) : v); // make footer bold
    tableData.push(footerColumns);
  }

  return DataOutput.formatData(tableData, format, columns);
}

function main(period) {
  const mite = miteApi(config.get());

  const requestOpts = getRequestOptions(period, program);
  // "columns" default option is different
  if (!program.columns) {
    // when groupBy is used, make sure that revenue and duration are used
    if (program.groupBy) {
      program.columns = program.groupBy + ',revenue,duration';
    } else {
      program.columns = config.get().listColumns;
    }
  }
  const columns = columnOptions.resolve(program.columns, listCommand.columns.options);

  mite.getTimeEntries(requestOpts, (err, results) => {
    if (err) return handleError(err);

    // decide wheter to output grouped report or single entry report
    let items = results
      .map(data => data[program.groupBy ? 'time_entry_group' : 'time_entry'])
      .filter(v => v)
      .filter(filterData.bind(this, program))
      ;
    const report = getReport(items, columns, program.format);
    console.log(report);
  });
} // main

const period = program.args[0];
main(period);
<commit_msg>fix(mite-list): allow passing of exact dates without replacing underscores<commit_after>#!/usr/bin/env node
'use strict';

const program = require('commander');
const chalk = require('chalk');
const miteApi = require('mite-api');

const pkg = require('./../package.json');
const config = require('./config');
const formater = require('./lib/formater');
const DataOutput = require('./lib/data-output');
const listCommand = require('./lib/commands/list');
const columnOptions = require('./lib/options/columns');
const commandOptions = require('./lib/options');
const { handleError } = require('./lib/errors');

// no other options or comamnd line arguments used, default "period" to "today"
if (process.argv.length === 2) {
  process.argv.splice(2, 0, 'today');
}

program
  .version(pkg.version)
  .description('list time entries', {
    period:
      `name of the period for which the time entries should be shown \
or a single date. F.e. "today" or "last_week or "2019-03-17". \
Defaults to "today" when no other options are used and to "all" \
when other options are used.`
  })
  .arguments('<period>')
  .option.apply(program, commandOptions.toArgs(commandOptions.billable),
    'show entries which are billable or not billable'
  )
  .option(
    '--columns <columns>',
    columnOptions.description(listCommand.columns.options),
    columnOptions.parse
  )
  .option(
    '--customer-id <id>',
    'customer id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option.apply(program, commandOptions.toArgs(commandOptions.format, null, config.get('outputFormat')))
  .option(
    '--from <period|YYYY-MM-DD>',
    'in combination with "to" used for selecting a specific time frame of ' +
    'time entries to return, same as "period" argument or a specific date in ' +
    'the format "YYYY-MM-DD". The period argument is ignored.'
  )
  .option(
    '--group-by <column>',
    'optional grouping parameter which should be used. Valid values are ' +
    listCommand.groupBy.options.join(', ')
  )
  .option(
    '-l, --limit <limit>',
    'optional number of items to show, default is infinite',
    undefined,
    ((val) => parseInt(val, 10))
  )
  .option(
    '--locked <true|false>',
    'filter entries by their locked status',
    (val => ['true', 'yes', 'ja', 'ok', '1'].indexOf(val.toLowerCase()) > -1),
    null
  )
  .option(
    '--min-duration <minDuration>',
    'filter out entries which have a duration below the given value (client side)',
  )
  .option(
    '--max-duration <maxDuration>',
    'filter out entries which have a duration above the given value (client side)',
  )
  .option(
    '--project-id <id>',
    'project id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option(
    '--to <period|YYYY-MM-DD>',
    'in combination with "from" used for selecting a specific time frame of ' +
    'time entries to return, same as "period" argument or a specific date in ' +
    'the format "YYYY-MM-DD". The period argument is ignored.'
  )
  .option.apply(program, commandOptions.toArgs(commandOptions.tracking))
  .option(
    '--reversed',
    'reverse the sorting direction',
  )
  .option(
    '-s --search <query>',
    'search within the notes, to filter by multiple criteria connected with OR use comma-separated query values',
    ((val) => {
      if (typeof val === 'string') {
        return val.split(/\s*,\s*/);
      }
      return val;
    })
  )
  .option(
    '--service-id <id>',
    'service id, can be either a single ID, or multiple comma-separated IDs.'
  )
  .option.apply(program, commandOptions.toArgs(
    commandOptions.sort,
    commandOptions.sort.description(listCommand.sort.options),
    listCommand.sort.default
  ))
  .option(
    '--user-id <id>',
    'optional single user id who’s time entries should be returned or ' +
    'multiple values comma-separated. Note that the current user may not ' +
    'have the permission ot read other user’s time entries which will result ' +
    'in an empty response',
    ((val) => {
      if (typeof val === 'string') {
        return val.split(/\s*,\s*/);
      }
      return val;
    })
  )
  .on('--help', function() {
    console.log(`
Examples:

  list all entries from the current month:
    mite list this_month

  list all entries which note contains the given search query:
    mite list this_year --search JIRA-123

  show all entries from two services 123 and 38171
    mite list last_month --service-id 123,38171

  show all users who tracked billable entries ordered by the amount of time
  they have tracked:
    mite list this_year --billable true --columns user,duration --group-by user --sort duration

  export all time-entries from the current month as csv:
    mite list last_week --format csv --columns user,id

  create a markdown report of all customer and their generated profits:
    mite list this_year --format md --group-by customer --sort revenue

  The output of mite list can be forwarded to other commands using xargs. The
  following example will delete all matchin entries:
    mite list this_month --search="query" --columns id --format text | xargs -n1 mite delete

  Get the notes for one specific service, project for the last month to put
  them on a bill or similar
    mite list last_month --project-id 2681601 --service-id 325329 --columns note --format text | sort -u

  Show a seperate report for all users showing the revenues and times per
  service for all users matching a query:
    mite users --search marc --columns id --format text | xargs mite list last_month --group-by service --user-id

`);
  })
  .parse(process.argv);

/**
 * Returns the request options for requesting the time entries or grouped
 * time entries for the given time entries and filtering options.
 *
 * @param {string} period
 * @param {object} program
 * @return {Object<String, any>} time entries or grouped time entries
 */
function getRequestOptions(period, program) {

  // mite’s "periods" strings using underscore like in "this_month", "this_week"
  // but sometimes the user enters a minus instead of underscore, then replace
  // it
  if (typeof period === 'string' && !period.match(/[\d-]+/)) {
    period = period.replace(/-/g, '_');
  }

  const data = {
    ...(typeof program.billable === 'boolean' && { billable: program.billable }),
    ...(program.customerId && { customer_id: program.customerId }),
    ...(program.reversed && { direction: 'asc' }),
    ...(program.groupBy && { group_by: program.groupBy }),
    ...(program.limit && { limit: program.limit }),
    ...(typeof program.locked === 'boolean' && { locked: program.locked }),
    ...(program.search && { note: program.search }),
    ...(program.projectId && { project_id: program.projectId }),
    ...(program.serviceId && { service_id: program.serviceId }),
    ...(program.sort && { sort: program.sort }),
    ...(typeof program.tracking === 'boolean' && { tracking: program.tracking }),
    ...(program.userId && { user_id: program.userId}),
  };

  // add optional time period using from & to
  if (program.from && program.to) {
    data.from = program.from;
    data.to = program.to;
  } else if (period) {
    data.at = period;
  }

  return data;
}

/**
 * Callback function to filter time entries which are shown depending on the
 * command line options
 *
 * @param {object} program
 * @param {MiteTimeEntry} entry
 * @return {boolean}
 */
function filterData(program, row) {
  let valid = true;
  if (program.minDuration) {
    const minDuration = formater.durationToMinutes(program.minDuration, 10);
    valid &= row.minutes >= minDuration;
  }
  if (program.maxDuration) {
    const maxDuration = formater.durationToMinutes(program.maxDuration, 10);
    valid &= row.minutes <= maxDuration;
  }
  return valid;
}

function getReport(items, columns, format) {
  const tableData = DataOutput.compileTableData(items, columns, format);

  // Table footer
  // add table footer if any of the table columns has a reducer
  if (columnOptions.hasReducer(columns)) {
    let footerColumns = DataOutput.getTableFooterColumns(items, columns);
    footerColumns = footerColumns.map(v => v ? chalk.bold(v) : v); // make footer bold
    tableData.push(footerColumns);
  }

  return DataOutput.formatData(tableData, format, columns);
}

function main(period) {
  const mite = miteApi(config.get());

  const requestOpts = getRequestOptions(period, program);
  // "columns" default option is different
  if (!program.columns) {
    // when groupBy is used, make sure that revenue and duration are used
    if (program.groupBy) {
      program.columns = program.groupBy + ',revenue,duration';
    } else {
      program.columns = config.get().listColumns;
    }
  }
  const columns = columnOptions.resolve(program.columns, listCommand.columns.options);

  mite.getTimeEntries(requestOpts, (err, results) => {
    if (err) return handleError(err);

    // decide wheter to output grouped report or single entry report
    let items = results
      .map(data => data[program.groupBy ? 'time_entry_group' : 'time_entry'])
      .filter(v => v)
      .filter(filterData.bind(this, program))
      ;
    const report = getReport(items, columns, program.format);
    console.log(report);
  });
} // main

const period = program.args[0];
main(period);
<|endoftext|>


<commit_before>Registrants = new Meteor.Collection("registrants");

Registrants.attachSchema(new SimpleSchema({
    'first_name' : {
        type: String,
        label: "First Name",
        max: 200
    },
    'last_name': {
        type: String,
        label: "Last Name",
        max: 200
    },
    'age': {
        label: "Age at time of event",
        type: Number,
        max: 115,
        min: 0
    },
    'ageGroup': {
        type: String,
        allowedValues: [
            'child',
            'youth',
            'teen',
            'youngAdult',
            'adult'
        ],
        optional: true,
        autoValue: function (doc) {
            // calculate age group based on age field
            return calculateAgeGroup(this.field("age").value);
        }
    },
     'gradeInSchool': {
        type: String,
        optional: true,
        label: "Grade in school this year",
        max: 200
    },
    'postalAddress': {
        type: String,
        optional: true,
        label: "Primary postal address",
        autoform: {
            afFieldInput: {
                type: "textarea"
            }
        }
    },
    'telephone': {
       type: String,
       optional: true,
       label: "Primary telephone number",
       max: 20
   },
    'registrationType': {
        type: String,
        allowedValues: [
            'commuter',
            'daily',
            'weekly'
        ]
    },
    'accommodations': {
        type: String,
        allowedValues: [
            'camping',
            'dorm',
            'semiprivate',
            'private',
            'jym',
            'yaf'
        ],
        optional: true,
        custom: function () {
            // Required if registration is daily or weekly
            if ( !this.isSet
                &&
                (
                this.field('registrationType').value === "daily"
                ||
                this.field('registrationType').value === "weekly")
               ) {
                return "required";
            }
        }
    },
    days: {
        type: [String],
        optional: true,
        autoform: {
            type: "select-checkbox-inline",
            options: function () {
                return [
                    {label: "Monday", value: "Monday"},
                    {label: "Tuesday", value: "Tuesday"},
                    {label: "Wednesday", value: "Wednesday"},
                    {label: "Thursday", value: "Thursday"},
                    {label: "Friday", value: "Friday"},
                    {label: "Saturday", value: "Saturday"}
                ];
            }
        }
    },
    foodPreference: {
        type: [String],
        optional: true,
        autoform: {
            type: "select-checkbox-inline",
            allowedValues: [
                'Omnivore',
                'Vegetarian',
                'Vegan',
                'Gluten-free',
                'Dairy-free',
                'Sugar-free',
                'Soy-free',
                'Raw vegetables',
                'Low-salt'
            ],
            options: function () {
                return [
                    {label: "Omnivore", value: "Omnivore"},
                    {label: "Vegetarian", value: "Vegetarian"},
                    {label: "Vegan", value: "Vegan"},
                    {label: "Gluten-free", value: "Gluten-free"},
                    {label: "Dairy-free", value: "Dairy-free"},
                    {label: "Sugar-free", value: "Sugar-free"},
                    {label: "Soy-free", value: "Soy-free"},
                    {label: "Raw vegetables", value: "Raw vegetables"},
                    {label: "Low-salt", value: "Low-salt"}
                ];
            }
        }
    },
    'registrantEmail': {
        type: String,
        regEx: SimpleSchema.RegEx.Email,
        label: "Registrant E-mail address",
        optional: true
    },
    'registrantAffiliation': {
        type: String,
        optional: true,
        label: "Affiliated Quaker meeting, worship group, or organization"
    },
    'firstTimeAttender': {
        type: Boolean,
        optional: true,
        defaultValue: false,
        label: "First time attender?"
    },
    'linens': {
        type: Boolean,
        optional: true,
        defaultValue: false,
        label: "Will you need linens? ($25 extra)"
    },
    'specialNeeds': {
        type: String,
        optional: true,
        label: "Tell us about any special needs that we can accommodate.",
        autoform: {
            afFieldInput: {
                type: "textarea"
            }
        }
    },
    'carbonTax': {
        type: Number,
        label: "Voluntary carbon tax ($USD)",
        optional: true
    },
    'fee' : {
        type: Number,
        label: "Fee",
        optional: true,
        autoValue: function (doc) {
            // Set up an empty registration object
            // to hold values from the submitted registration
            var registration = {};

            // get values from submitted registration and
            // set attributes on the registration object
            registration.age = this.field("age").value;
            registration.ageGroup = calculateAgeGroup(registration.age);
            registration.type = this.field("registrationType").value;
            registration.accommodations = this.field("accommodations").value;
            registration.days =  this.field("days").value;
            registration.firstTimeAttender = this.field("firstTimeAttender").value;
            registration.linens = this.field("linens").value;
            registration.carbonTax = this.field("carbonTax").value;

            // calculate the registration fee
            // based on the submitted registration
            return calculateRegistrationPrice(registration);
        }
    }
}));

Registrants.before.insert(function (userId, doc){
    // set registration created date to current date
    doc.createdAt = Date.now();

    //  Attach registration to Meteor user
    // for reporting, access control, etc
    doc.createdById = userId;

    // Add user email address to registration document
    doc.createdByEmail = Meteor.users.findOne(userId).emails[0].address;
});
<commit_msg>Registration allow rule.<commit_after>Registrants = new Meteor.Collection("registrants");

Registrants.allow({
    // User must be logged in to register
    insert: function (userId, doc) {
        return !! userId;
    }
});

Registrants.attachSchema(new SimpleSchema({
    'first_name' : {
        type: String,
        label: "First Name",
        max: 200
    },
    'last_name': {
        type: String,
        label: "Last Name",
        max: 200
    },
    'age': {
        label: "Age at time of event",
        type: Number,
        max: 115,
        min: 0
    },
    'ageGroup': {
        type: String,
        allowedValues: [
            'child',
            'youth',
            'teen',
            'youngAdult',
            'adult'
        ],
        optional: true,
        autoValue: function (doc) {
            // calculate age group based on age field
            return calculateAgeGroup(this.field("age").value);
        }
    },
    'gradeInSchool': {
        type: String,
        optional: true,
        label: "Grade in school this year",
        max: 200
    },
    'postalAddress': {
        type: String,
        optional: true,
        label: "Primary postal address",
        autoform: {
            afFieldInput: {
                type: "textarea"
            }
        }
    },
    'telephone': {
        type: String,
        optional: true,
        label: "Primary telephone number",
        max: 20
    },
    'registrationType': {
        type: String,
        allowedValues: [
            'commuter',
            'daily',
            'weekly'
        ]
    },
    'accommodations': {
        type: String,
        allowedValues: [
            'camping',
            'dorm',
            'semiprivate',
            'private',
            'jym',
            'yaf'
        ],
        optional: true,
        custom: function () {
            // Required if registration is daily or weekly
            if ( !this.isSet
                &&
                (
                this.field('registrationType').value === "daily"
                ||
                this.field('registrationType').value === "weekly")
               ) {
                return "required";
            }
        }
    },
    days: {
        type: [String],
        optional: true,
        autoform: {
            type: "select-checkbox-inline",
            options: function () {
                return [
                    {label: "Monday", value: "Monday"},
                    {label: "Tuesday", value: "Tuesday"},
                    {label: "Wednesday", value: "Wednesday"},
                    {label: "Thursday", value: "Thursday"},
                    {label: "Friday", value: "Friday"},
                    {label: "Saturday", value: "Saturday"}
                ];
            }
        }
    },
    foodPreference: {
        type: [String],
        optional: true,
        autoform: {
            type: "select-checkbox-inline",
            allowedValues: [
                'Omnivore',
                'Vegetarian',
                'Vegan',
                'Gluten-free',
                'Dairy-free',
                'Sugar-free',
                'Soy-free',
                'Raw vegetables',
                'Low-salt'
            ],
            options: function () {
                return [
                    {label: "Omnivore", value: "Omnivore"},
                    {label: "Vegetarian", value: "Vegetarian"},
                    {label: "Vegan", value: "Vegan"},
                    {label: "Gluten-free", value: "Gluten-free"},
                    {label: "Dairy-free", value: "Dairy-free"},
                    {label: "Sugar-free", value: "Sugar-free"},
                    {label: "Soy-free", value: "Soy-free"},
                    {label: "Raw vegetables", value: "Raw vegetables"},
                    {label: "Low-salt", value: "Low-salt"}
                ];
            }
        }
    },
    'registrantEmail': {
        type: String,
        regEx: SimpleSchema.RegEx.Email,
        label: "Registrant E-mail address",
        optional: true
    },
    'registrantAffiliation': {
        type: String,
        optional: true,
        label: "Affiliated Quaker meeting, worship group, or organization"
    },
    'firstTimeAttender': {
        type: Boolean,
        optional: true,
        defaultValue: false,
        label: "First time attender?"
    },
    'linens': {
        type: Boolean,
        optional: true,
        defaultValue: false,
        label: "Will you need linens? ($25 extra)"
    },
    'specialNeeds': {
        type: String,
        optional: true,
        label: "Tell us about any special needs that we can accommodate.",
        autoform: {
            afFieldInput: {
                type: "textarea"
            }
        }
    },
    'carbonTax': {
        type: Number,
        label: "Voluntary carbon tax ($USD)",
        optional: true
    },
    'fee' : {
        type: Number,
        label: "Fee",
        optional: true,
        autoValue: function (doc) {
            // Set up an empty registration object
            // to hold values from the submitted registration
            var registration = {};

            // get values from submitted registration and
            // set attributes on the registration object
            registration.age = this.field("age").value;
            registration.ageGroup = calculateAgeGroup(registration.age);
            registration.type = this.field("registrationType").value;
            registration.accommodations = this.field("accommodations").value;
            registration.days =  this.field("days").value;
            registration.firstTimeAttender = this.field("firstTimeAttender").value;
            registration.linens = this.field("linens").value;
            registration.carbonTax = this.field("carbonTax").value;

            // calculate the registration fee
            // based on the submitted registration
            return calculateRegistrationPrice(registration);
        }
    }
}));

Registrants.before.insert(function (userId, doc){
    // set registration created date to current date
    doc.createdAt = Date.now();

    //  Attach registration to Meteor user
    // for reporting, access control, etc
    doc.createdById = userId;

    // Add user email address to registration document
    doc.createdByEmail = Meteor.users.findOne(userId).emails[0].address;
});
<|endoftext|>


<commit_before>const assert = require('assert')
const findLast = require('lodash/findLast')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const keyBy = require('lodash/keyBy')
const mapValues = require('lodash/mapValues')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { formatDateTime } = require('@xen-orchestra/xapi')

const { ContinuousReplicationWriter } = require('./_ContinuousReplicationWriter')
const { DeltaBackupWriter } = require('./_DeltaBackupWriter')
const { DisasterRecoveryWriter } = require('./_DisasterRecoveryWriter')
const { exportDeltaVm } = require('./_deltaVm')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe')
const { FullBackupWriter } = require('./_FullBackupWriter')
const { getOldEntries } = require('./_getOldEntries')
const { Task } = require('./Task')
const { watchStreamSize } = require('./_watchStreamSize')

const { debug, warn } = createLogger('xo:backups:VmBackup')

const forkDeltaExport = deltaExport =>
  Object.create(deltaExport, {
    streams: {
      value: mapValues(deltaExport.streams, forkStreamUnpipe),
    },
  })

exports.VmBackup = class VmBackup {
  constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
    this.config = config
    this.job = job
    this.remoteAdapters = remoteAdapters
    this.remotes = remotes
    this.scheduleId = schedule.id
    this.timestamp = undefined

    // VM currently backed up
    this.vm = vm
    const { tags } = this.vm

    // VM (snapshot) that is really exported
    this.exportedVm = undefined

    this._fullVdisRequired = undefined
    this._getSnapshotNameLabel = getSnapshotNameLabel
    this._isDelta = job.mode === 'delta'
    this._jobId = job.id
    this._jobSnapshots = undefined
    this._xapi = vm.$xapi

    // Base VM for the export
    this._baseVm = undefined

    // Settings for this specific run (job, schedule, VM)
    if (tags.includes('xo-memory-backup')) {
      settings.checkpointSnapshot = true
    }
    if (tags.includes('xo-offline-backup')) {
      settings.offlineSnapshot = true
    }
    this._settings = settings

    // Create writers
    {
      const writers = []
      this._writers = writers

      const [BackupWriter, ReplicationWriter] = this._isDelta
        ? [DeltaBackupWriter, ContinuousReplicationWriter]
        : [FullBackupWriter, DisasterRecoveryWriter]

      const allSettings = job.settings

      Object.keys(remoteAdapters).forEach(remoteId => {
        const targetSettings = {
          ...settings,
          ...allSettings[remoteId],
        }
        if (targetSettings.exportRetention !== 0) {
          writers.push(new BackupWriter(this, remoteId, targetSettings))
        }
      })
      srs.forEach(sr => {
        const targetSettings = {
          ...settings,
          ...allSettings[sr.uuid],
        }
        if (targetSettings.copyRetention !== 0) {
          writers.push(new ReplicationWriter(this, sr, targetSettings))
        }
      })
    }
  }

  // ensure the VM itself does not have any backup metadata which would be
  // copied on manual snapshots and interfere with the backup jobs
  async _cleanMetadata() {
    const { vm } = this
    if ('xo:backup:job' in vm.other_config) {
      await vm.update_other_config({
        'xo:backup:datetime': null,
        'xo:backup:deltaChainLength': null,
        'xo:backup:exported': null,
        'xo:backup:job': null,
        'xo:backup:schedule': null,
        'xo:backup:vm': null,
      })
    }
  }

  async _snapshot() {
    const { vm } = this
    const xapi = this._xapi

    const settings = this._settings

    assert.notStrictEqual(settings.offlineBackup, settings.snapshotRetention !== 0)

    const doSnapshot =
      this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
    if (doSnapshot) {
      await Task.run({ name: 'snapshot' }, async () => {
        if (!settings.bypassVdiChainsCheck) {
          await vm.$assertHealthyVdiChains()
        }

        const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
          name_label: this._getSnapshotNameLabel(vm),
        })
        this.timestamp = Date.now()

        await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
          'xo:backup:datetime': formatDateTime(this.timestamp),
          'xo:backup:job': this._jobId,
          'xo:backup:schedule': this.scheduleId,
          'xo:backup:vm': vm.uuid,
        })

        this.exportedVm = await xapi.getRecord('VM', snapshotRef)

        return this.exportedVm.uuid
      })
    } else {
      this.exportedVm = vm
      this.timestamp = Date.now()
    }
  }

  async _copyDelta() {
    const { exportedVm } = this
    const baseVm = this._baseVm
    const fullVdisRequired = this._fullVdisRequired

    const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0

    await asyncMap(this._writers, writer => writer.prepare && writer.prepare({ isFull }))

    const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
      fullVdisRequired,
    })
    const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))

    const timestamp = Date.now()

    await asyncMap(this._writers, async writer => {
      try {
        await writer.transfer({
          deltaExport: forkDeltaExport(deltaExport),
          sizeContainers,
          timestamp,
        })
      } catch (error) {
        warn('copy failure', {
          error,
          target: writer.target,
          vm: this.vm,
        })
      }
    })

    this._baseVm = exportedVm

    if (baseVm !== undefined) {
      await exportedVm.update_other_config(
        'xo:backup:deltaChainLength',
        String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
      )
    }

    // not the case if offlineBackup
    if (exportedVm.is_a_snapshot) {
      await exportedVm.update_other_config('xo:backup:exported', 'true')
    }

    const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
    const end = Date.now()
    const duration = end - timestamp
    debug('transfer complete', {
      duration,
      speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
      size,
    })

    await asyncMap(this._writers, writer => writer.cleanup && writer.cleanup())
  }

  async _copyFull() {
    const { compression } = this.job
    const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
      compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
      useSnapshot: false,
    })
    const sizeContainer = watchStreamSize(stream)

    const timestamp = Date.now()

    await asyncMap(this._writers, async writer => {
      try {
        await writer.run({
          sizeContainer,
          stream: forkStreamUnpipe(stream),
          timestamp,
        })
      } catch (error) {
        warn('copy failure', {
          error,
          target: writer.target,
          vm: this.vm,
        })
      }
    })

    const { size } = sizeContainer
    const end = Date.now()
    const duration = end - timestamp
    debug('transfer complete', {
      duration,
      speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
      size,
    })
  }

  async _fetchJobSnapshots() {
    const jobId = this._jobId
    const vmRef = this.vm.$ref
    const xapi = this._xapi

    const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
    const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))

    const snapshots = []
    snapshotsOtherConfig.forEach((other_config, i) => {
      if (other_config['xo:backup:job'] === jobId) {
        snapshots.push({ other_config, $ref: snapshotsRef[i] })
      }
    })
    snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
    this._jobSnapshots = snapshots
  }

  async _removeUnusedSnapshots() {
    // TODO: handle all schedules (no longer existing schedules default to 0 retention)

    const { scheduleId } = this
    const scheduleSnapshots = this._jobSnapshots.filter(_ => _.other_config['xo:backup:schedule'] === scheduleId)

    const baseVmRef = this._baseVm?.$ref
    const xapi = this._xapi
    await asyncMap(getOldEntries(this._settings.snapshotRetention, scheduleSnapshots), ({ $ref }) => {
      if ($ref !== baseVmRef) {
        return xapi.VM_destroy($ref)
      }
    })
  }

  async _selectBaseVm() {
    const xapi = this._xapi

    let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
    if (baseVm === undefined) {
      return
    }

    const fullInterval = this._settings.fullInterval
    const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
    if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
      return
    }

    const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')

    // resolve full record
    baseVm = await xapi.getRecord('VM', baseVm.$ref)

    const baseUuidToSrcVdi = new Map()
    await asyncMap(await baseVm.$getDisks(), async baseRef => {
      const snapshotOf = await xapi.getField('VDI', baseRef, 'snapshot_of')
      const srcVdi = srcVdis[snapshotOf]
      if (srcVdi !== undefined) {
        baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
      }
    })

    const presentBaseVdis = new Map(baseUuidToSrcVdi)
    const writers = this._writers
    for (let i = 0, n = writers.length; presentBaseVdis.size !== 0 && i < n; ++i) {
      await writers[i].checkBaseVdis(presentBaseVdis, baseVm)
    }

    if (presentBaseVdis.size === 0) {
      return
    }

    const fullVdisRequired = new Set()
    baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
      if (!presentBaseVdis.has(baseUuid)) {
        fullVdisRequired.add(srcVdi.uuid)
      }
    })

    this._baseVm = baseVm
    this._fullVdisRequired = fullVdisRequired
  }

  async run() {
    await this._fetchJobSnapshots()

    if (this._isDelta) {
      await this._selectBaseVm()
    }

    await this._cleanMetadata()
    await this._removeUnusedSnapshots()

    const { _settings: settings, vm } = this
    const isRunning = vm.power_state === 'Running'
    const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
    if (startAfter) {
      await vm.$callAsync('clean_shutdown')
    }

    try {
      await this._snapshot()
      if (startAfter === 'snapshot') {
        ignoreErrors.call(vm.$callAsync('start', false, false))
      }

      if (this._writers.length !== 0) {
        await (this._isDelta ? this._copyDelta() : this._copyFull())
      }
    } finally {
      if (startAfter) {
        ignoreErrors.call(vm.$callAsync('start', false, false))
      }

      await this._fetchJobSnapshots()
      await this._removeUnusedSnapshots()
    }
  }
}
<commit_msg>fix(backups/VmBackup#_snapshot): dont fail on !offlineBackup && !snapshotRetention<commit_after>const findLast = require('lodash/findLast')
const ignoreErrors = require('promise-toolbox/ignoreErrors')
const keyBy = require('lodash/keyBy')
const mapValues = require('lodash/mapValues')
const { asyncMap } = require('@xen-orchestra/async-map')
const { createLogger } = require('@xen-orchestra/log')
const { formatDateTime } = require('@xen-orchestra/xapi')

const { ContinuousReplicationWriter } = require('./_ContinuousReplicationWriter')
const { DeltaBackupWriter } = require('./_DeltaBackupWriter')
const { DisasterRecoveryWriter } = require('./_DisasterRecoveryWriter')
const { exportDeltaVm } = require('./_deltaVm')
const { forkStreamUnpipe } = require('./_forkStreamUnpipe')
const { FullBackupWriter } = require('./_FullBackupWriter')
const { getOldEntries } = require('./_getOldEntries')
const { Task } = require('./Task')
const { watchStreamSize } = require('./_watchStreamSize')

const { debug, warn } = createLogger('xo:backups:VmBackup')

const forkDeltaExport = deltaExport =>
  Object.create(deltaExport, {
    streams: {
      value: mapValues(deltaExport.streams, forkStreamUnpipe),
    },
  })

exports.VmBackup = class VmBackup {
  constructor({ config, getSnapshotNameLabel, job, remoteAdapters, remotes, schedule, settings, srs, vm }) {
    this.config = config
    this.job = job
    this.remoteAdapters = remoteAdapters
    this.remotes = remotes
    this.scheduleId = schedule.id
    this.timestamp = undefined

    // VM currently backed up
    this.vm = vm
    const { tags } = this.vm

    // VM (snapshot) that is really exported
    this.exportedVm = undefined

    this._fullVdisRequired = undefined
    this._getSnapshotNameLabel = getSnapshotNameLabel
    this._isDelta = job.mode === 'delta'
    this._jobId = job.id
    this._jobSnapshots = undefined
    this._xapi = vm.$xapi

    // Base VM for the export
    this._baseVm = undefined

    // Settings for this specific run (job, schedule, VM)
    if (tags.includes('xo-memory-backup')) {
      settings.checkpointSnapshot = true
    }
    if (tags.includes('xo-offline-backup')) {
      settings.offlineSnapshot = true
    }
    this._settings = settings

    // Create writers
    {
      const writers = []
      this._writers = writers

      const [BackupWriter, ReplicationWriter] = this._isDelta
        ? [DeltaBackupWriter, ContinuousReplicationWriter]
        : [FullBackupWriter, DisasterRecoveryWriter]

      const allSettings = job.settings

      Object.keys(remoteAdapters).forEach(remoteId => {
        const targetSettings = {
          ...settings,
          ...allSettings[remoteId],
        }
        if (targetSettings.exportRetention !== 0) {
          writers.push(new BackupWriter(this, remoteId, targetSettings))
        }
      })
      srs.forEach(sr => {
        const targetSettings = {
          ...settings,
          ...allSettings[sr.uuid],
        }
        if (targetSettings.copyRetention !== 0) {
          writers.push(new ReplicationWriter(this, sr, targetSettings))
        }
      })
    }
  }

  // ensure the VM itself does not have any backup metadata which would be
  // copied on manual snapshots and interfere with the backup jobs
  async _cleanMetadata() {
    const { vm } = this
    if ('xo:backup:job' in vm.other_config) {
      await vm.update_other_config({
        'xo:backup:datetime': null,
        'xo:backup:deltaChainLength': null,
        'xo:backup:exported': null,
        'xo:backup:job': null,
        'xo:backup:schedule': null,
        'xo:backup:vm': null,
      })
    }
  }

  async _snapshot() {
    const { vm } = this
    const xapi = this._xapi

    const settings = this._settings

    const doSnapshot =
      this._isDelta || (!settings.offlineBackup && vm.power_state === 'Running') || settings.snapshotRetention !== 0
    if (doSnapshot) {
      await Task.run({ name: 'snapshot' }, async () => {
        if (!settings.bypassVdiChainsCheck) {
          await vm.$assertHealthyVdiChains()
        }

        const snapshotRef = await vm[settings.checkpointSnapshot ? '$checkpoint' : '$snapshot']({
          name_label: this._getSnapshotNameLabel(vm),
        })
        this.timestamp = Date.now()

        await xapi.setFieldEntries('VM', snapshotRef, 'other_config', {
          'xo:backup:datetime': formatDateTime(this.timestamp),
          'xo:backup:job': this._jobId,
          'xo:backup:schedule': this.scheduleId,
          'xo:backup:vm': vm.uuid,
        })

        this.exportedVm = await xapi.getRecord('VM', snapshotRef)

        return this.exportedVm.uuid
      })
    } else {
      this.exportedVm = vm
      this.timestamp = Date.now()
    }
  }

  async _copyDelta() {
    const { exportedVm } = this
    const baseVm = this._baseVm
    const fullVdisRequired = this._fullVdisRequired

    const isFull = fullVdisRequired === undefined || fullVdisRequired.size !== 0

    await asyncMap(this._writers, writer => writer.prepare && writer.prepare({ isFull }))

    const deltaExport = await exportDeltaVm(exportedVm, baseVm, {
      fullVdisRequired,
    })
    const sizeContainers = mapValues(deltaExport.streams, stream => watchStreamSize(stream))

    const timestamp = Date.now()

    await asyncMap(this._writers, async writer => {
      try {
        await writer.transfer({
          deltaExport: forkDeltaExport(deltaExport),
          sizeContainers,
          timestamp,
        })
      } catch (error) {
        warn('copy failure', {
          error,
          target: writer.target,
          vm: this.vm,
        })
      }
    })

    this._baseVm = exportedVm

    if (baseVm !== undefined) {
      await exportedVm.update_other_config(
        'xo:backup:deltaChainLength',
        String(+(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1)
      )
    }

    // not the case if offlineBackup
    if (exportedVm.is_a_snapshot) {
      await exportedVm.update_other_config('xo:backup:exported', 'true')
    }

    const size = Object.values(sizeContainers).reduce((sum, { size }) => sum + size, 0)
    const end = Date.now()
    const duration = end - timestamp
    debug('transfer complete', {
      duration,
      speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
      size,
    })

    await asyncMap(this._writers, writer => writer.cleanup && writer.cleanup())
  }

  async _copyFull() {
    const { compression } = this.job
    const stream = await this._xapi.VM_export(this.exportedVm.$ref, {
      compress: Boolean(compression) && (compression === 'native' ? 'gzip' : 'zstd'),
      useSnapshot: false,
    })
    const sizeContainer = watchStreamSize(stream)

    const timestamp = Date.now()

    await asyncMap(this._writers, async writer => {
      try {
        await writer.run({
          sizeContainer,
          stream: forkStreamUnpipe(stream),
          timestamp,
        })
      } catch (error) {
        warn('copy failure', {
          error,
          target: writer.target,
          vm: this.vm,
        })
      }
    })

    const { size } = sizeContainer
    const end = Date.now()
    const duration = end - timestamp
    debug('transfer complete', {
      duration,
      speed: duration !== 0 ? (size * 1e3) / 1024 / 1024 / duration : 0,
      size,
    })
  }

  async _fetchJobSnapshots() {
    const jobId = this._jobId
    const vmRef = this.vm.$ref
    const xapi = this._xapi

    const snapshotsRef = await xapi.getField('VM', vmRef, 'snapshots')
    const snapshotsOtherConfig = await asyncMap(snapshotsRef, ref => xapi.getField('VM', ref, 'other_config'))

    const snapshots = []
    snapshotsOtherConfig.forEach((other_config, i) => {
      if (other_config['xo:backup:job'] === jobId) {
        snapshots.push({ other_config, $ref: snapshotsRef[i] })
      }
    })
    snapshots.sort((a, b) => (a.other_config['xo:backup:datetime'] < b.other_config['xo:backup:datetime'] ? -1 : 1))
    this._jobSnapshots = snapshots
  }

  async _removeUnusedSnapshots() {
    // TODO: handle all schedules (no longer existing schedules default to 0 retention)

    const { scheduleId } = this
    const scheduleSnapshots = this._jobSnapshots.filter(_ => _.other_config['xo:backup:schedule'] === scheduleId)

    const baseVmRef = this._baseVm?.$ref
    const xapi = this._xapi
    await asyncMap(getOldEntries(this._settings.snapshotRetention, scheduleSnapshots), ({ $ref }) => {
      if ($ref !== baseVmRef) {
        return xapi.VM_destroy($ref)
      }
    })
  }

  async _selectBaseVm() {
    const xapi = this._xapi

    let baseVm = findLast(this._jobSnapshots, _ => 'xo:backup:exported' in _.other_config)
    if (baseVm === undefined) {
      return
    }

    const fullInterval = this._settings.fullInterval
    const deltaChainLength = +(baseVm.other_config['xo:backup:deltaChainLength'] ?? 0) + 1
    if (!(fullInterval === 0 || fullInterval > deltaChainLength)) {
      return
    }

    const srcVdis = keyBy(await xapi.getRecords('VDI', await this.vm.$getDisks()), '$ref')

    // resolve full record
    baseVm = await xapi.getRecord('VM', baseVm.$ref)

    const baseUuidToSrcVdi = new Map()
    await asyncMap(await baseVm.$getDisks(), async baseRef => {
      const snapshotOf = await xapi.getField('VDI', baseRef, 'snapshot_of')
      const srcVdi = srcVdis[snapshotOf]
      if (srcVdi !== undefined) {
        baseUuidToSrcVdi.set(await xapi.getField('VDI', baseRef, 'uuid'), srcVdi)
      }
    })

    const presentBaseVdis = new Map(baseUuidToSrcVdi)
    const writers = this._writers
    for (let i = 0, n = writers.length; presentBaseVdis.size !== 0 && i < n; ++i) {
      await writers[i].checkBaseVdis(presentBaseVdis, baseVm)
    }

    if (presentBaseVdis.size === 0) {
      return
    }

    const fullVdisRequired = new Set()
    baseUuidToSrcVdi.forEach((srcVdi, baseUuid) => {
      if (!presentBaseVdis.has(baseUuid)) {
        fullVdisRequired.add(srcVdi.uuid)
      }
    })

    this._baseVm = baseVm
    this._fullVdisRequired = fullVdisRequired
  }

  async run() {
    await this._fetchJobSnapshots()

    if (this._isDelta) {
      await this._selectBaseVm()
    }

    await this._cleanMetadata()
    await this._removeUnusedSnapshots()

    const { _settings: settings, vm } = this
    const isRunning = vm.power_state === 'Running'
    const startAfter = isRunning && (settings.offlineBackup ? 'backup' : settings.offlineSnapshot && 'snapshot')
    if (startAfter) {
      await vm.$callAsync('clean_shutdown')
    }

    try {
      await this._snapshot()
      if (startAfter === 'snapshot') {
        ignoreErrors.call(vm.$callAsync('start', false, false))
      }

      if (this._writers.length !== 0) {
        await (this._isDelta ? this._copyDelta() : this._copyFull())
      }
    } finally {
      if (startAfter) {
        ignoreErrors.call(vm.$callAsync('start', false, false))
      }

      await this._fetchJobSnapshots()
      await this._removeUnusedSnapshots()
    }
  }
}
<|endoftext|>


<commit_before>const request = require('request');
const fs = require('fs');

module.exports = {
  getReleases,
  getSingleRelease,
  getLatestRelease,
  getReleaseByTag,
  downloadLatestRelease,
  downloadReleaseByTag,
  downloadSingleRelease
}

function getReleases(params, callback) {
  const url = createBaseUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getSingleRelease(params, callback) {
  const url = createIdUrl(params.user, params.repo, params.id);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getLatestRelease(params, callback) {
  const url = createLatestUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getReleaseByTag(params, callback) {
  const url = createTagUrl(params.user, params.repo, params.tag);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function downloadLatestRelease(params, callback) {
  const url = createLatestUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function downloadReleaseByTag(params, callback) {
  const url = createTagUrl(params.user, params.repo, params.tag);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function downloadSingleRelease(params, callback) {
  const url = createIdUrl(params.user, params.repo, params.id);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function sendDownloadRequest(options, params, callback) {
  request(options, function(error, response, body) {

    if(!error && response.statusCode == 200) {
      const json = JSON.parse(body);
      const urls = [];

      if(params.zipball)
        urls.push(json.zipball_url);
      if(params.tarball)
        urls.push(json.tarball_url);

      if(!fs.existsSync(params.saveDirectory))
        fs.mkdirSync(params.saveDirectory);

      urls.forEach(function(value) {
        options.url = value;
        let savePath = '';
        if(value == json.zipball_url)
          savePath = params.saveDirectory + params.zipball.fileName;
        else if(value == json.tarball_url)
          savePath = params.saveDirectory + params.tarball.fileName;
        const stream = request(options).pipe(fs.createWriteStream(savePath));
        stream.on('finish', function() {
          callback(null, value);
        });
        stream.on('error', function(e) {
          callback(e, null);
        });
      });
    } else {
      callback(error, response);
    }

  });
}

function createOptionsFromUrl(url) {
  return {
    url: url,
    headers: {
      'User-Agent': 'git-release'
    }
  }
}

function createBaseUrl(user, repo) {
  return 'https://api.github.com/repos/' + user + '/' + repo + '/releases';
}

function createLatestUrl(user, repo) {
  return createBaseUrl(user, repo) + '/latest';
}

function createTagUrl(user, repo, tag) {
  return createBaseUrl(user, repo) + '/tags/' + tag;
}

function createIdUrl(user, repo, id) {
  return createBaseUrl(user, repo) + '/' + id;
}
<commit_msg>add strict mode<commit_after>'use strict'

const request = require('request');
const fs = require('fs');

module.exports = {
  getReleases,
  getSingleRelease,
  getLatestRelease,
  getReleaseByTag,
  downloadLatestRelease,
  downloadReleaseByTag,
  downloadSingleRelease
}

function getReleases(params, callback) {
  const url = createBaseUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getSingleRelease(params, callback) {
  const url = createIdUrl(params.user, params.repo, params.id);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getLatestRelease(params, callback) {
  const url = createLatestUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function getReleaseByTag(params, callback) {
  const url = createTagUrl(params.user, params.repo, params.tag);
  const options = createOptionsFromUrl(url);
  request(options, function(error, response, body) {
    if(callback) callback(error, response);
  });
}

function downloadLatestRelease(params, callback) {
  const url = createLatestUrl(params.user, params.repo);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function downloadReleaseByTag(params, callback) {
  const url = createTagUrl(params.user, params.repo, params.tag);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function downloadSingleRelease(params, callback) {
  const url = createIdUrl(params.user, params.repo, params.id);
  const options = createOptionsFromUrl(url);
  sendDownloadRequest(options, params, callback);
}

function sendDownloadRequest(options, params, callback) {
  request(options, function(error, response, body) {

    if(!error && response.statusCode == 200) {
      const json = JSON.parse(body);
      const urls = [];

      if(params.zipball)
        urls.push(json.zipball_url);
      if(params.tarball)
        urls.push(json.tarball_url);

      if(!fs.existsSync(params.saveDirectory))
        fs.mkdirSync(params.saveDirectory);

      urls.forEach(function(value) {
        options.url = value;
        let savePath = '';
        if(value == json.zipball_url)
          savePath = params.saveDirectory + params.zipball.fileName;
        else if(value == json.tarball_url)
          savePath = params.saveDirectory + params.tarball.fileName;
        const stream = request(options).pipe(fs.createWriteStream(savePath));
        stream.on('finish', function() {
          callback(null, value);
        });
        stream.on('error', function(e) {
          callback(e, null);
        });
      });
    } else {
      callback(error, response);
    }

  });
}

function createOptionsFromUrl(url) {
  return {
    url: url,
    headers: {
      'User-Agent': 'git-release'
    }
  }
}

function createBaseUrl(user, repo) {
  return 'https://api.github.com/repos/' + user + '/' + repo + '/releases';
}

function createLatestUrl(user, repo) {
  return createBaseUrl(user, repo) + '/latest';
}

function createTagUrl(user, repo, tag) {
  return createBaseUrl(user, repo) + '/tags/' + tag;
}

function createIdUrl(user, repo, id) {
  return createBaseUrl(user, repo) + '/' + id;
}
<|endoftext|>