diff --git a/app/assets/javascripts/filesets.js b/app/assets/javascripts/filesets.js index 1cf8042..0ffab2e 100644 --- a/app/assets/javascripts/filesets.js +++ b/app/assets/javascripts/filesets.js @@ -1,47 +1,47 @@ $(document).ready(function() { if ($('.include_files-plus-sign').size() > 0) { $(".include_files-plus-sign").click(function() { addIncludedFileTextArea(); }); }; if ($('.exclude_directions-plus-sign').size() > 0) { $(".exclude_directions-plus-sign").click(function() { addExcludeDirectionsTextArea(); }); }; if ($('.include_files-remove-sign').size() > 0) { $(".include_files-remove-sign").click(function() { removeIncludedFileTextArea($(this)); }); }; if ($('.exclude_directions-remove-sign').size() > 0) { $(".exclude_directions-remove-sign").click(function() { removeExcludeDirectionsTextArea($(this)); }); }; }); function addIncludedFileTextArea() { var textArrea = $('.templates .include_files:last').clone(true).val(""); textArrea.insertBefore('.here-included'); if ($('.include_files').size() > 1) { $('.include_files-remove-sign').show(); }; } function addExcludeDirectionsTextArea() { var textArrea = $('.templates .exclude_directions:last').clone(true).val(""); textArrea.insertAfter('.here-excluded'); $('.exclude_directions:last input').val(""); } function removeIncludedFileTextArea(element) { element.closest('div.include_files').remove(); - if ($('.include_files').size() <= 1) { + if ($('form .include_files').size() <= 1) { $('.include_files-remove-sign').hide(); }; } function removeExcludeDirectionsTextArea(element) { element.closest('div.exclude_directions').remove() } diff --git a/app/controllers/filesets_controller.rb b/app/controllers/filesets_controller.rb index 3cde529..438f879 100644 --- a/app/controllers/filesets_controller.rb +++ b/app/controllers/filesets_controller.rb @@ -1,94 +1,94 @@ class FilesetsController < ApplicationController before_action :require_logged_in before_action :fetch_host, only: [:new, :create, :show, :edit, :update] before_action :fetch_job_id, only: [:new, :create, :edit, :update] before_action :fetch_fileset, only: [:show, :edit, :update] # GET /hosts/:host_id/filesets/new def new @fileset = @host.filesets.new @fileset.include_directions = { 'file' => [nil] } @fileset.exclude_directions = [''] end # GET /hosts/:host_id/filesets/:id def show @fileset = @host.filesets.find(params[:id]) respond_to do |format| format.js {} end end # GET /hosts/:host_id/filesets/:id/edit def edit @fileset.include_files = @fileset.include_directions['file'] - @fileset.exclude_directions ||= [''] + @fileset.exclude_directions = [''] if @fileset.exclude_directions.empty? end # PATCH /hosts/:host_id/filesets/:id/ def update fileset_params = fetch_params if fileset_params[:exclude_directions].nil? fileset_params[:exclude_directions] = [] end if @fileset.update(fileset_params) flash[:success] = 'Fileset updated successfully' participating_hosts = @fileset.participating_hosts if participating_hosts.size.nonzero? participating_hosts.each(&:recalculate) flash[:alert] = 'You will need to redeploy the afffected clients: ' + participating_hosts.map(&:name).join(', ') end if @job_id redirect_to edit_host_job_path(@host, @job_id, fileset_id: @fileset.id) else redirect_to new_host_job_path(@host, fileset_id: @fileset.id) end else render :edit end end # POST /hosts/:host_id/filesets def create @fileset = @host.filesets.new(fetch_params) if @fileset.save flash[:success] = 'Fileset created' if @job_id.present? redirect_to edit_host_job_path(@host, @job_id, fileset_id: @fileset.id) else redirect_to new_host_job_path(@host, fileset_id: @fileset.id) end else @fileset.include_files = nil @fileset.exclude_directions = [''] @fileset.include_directions = { 'file' => [nil] } render :new end end # DELETE /hosts/:host_id/filesets/:id def destroy end private def fetch_host @host = current_user.hosts.find(params[:host_id]) end def fetch_job_id @job_id = @host.job_templates.find(params[:job_id]).id if params[:job_id].present? end def fetch_fileset @fileset = @host.filesets.find(params[:id]) end def fetch_params params.require(:fileset).permit(:name, exclude_directions: [], include_files: []) end end diff --git a/app/models/fileset.rb b/app/models/fileset.rb index 981b640..659f905 100644 --- a/app/models/fileset.rb +++ b/app/models/fileset.rb @@ -1,140 +1,143 @@ # Fileset model is the application representation of Bacula's Fileset. # It has references to a host and job templates. class Fileset < ActiveRecord::Base establish_connection ARCHIVING_CONF serialize :exclude_directions, Array serialize :include_directions, JSON attr_accessor :include_files belongs_to :host has_many :job_templates validates :name, presence: true, uniqueness: { scope: :host } validate :has_included_files, on: :create validates_with NameValidator before_save :sanitize_exclude_directions, :sanitize_include_directions DEFAULT_EXCLUDED = %w{/var/lib/bacula /proc /tmp /.journal /.fsck /bacula} DEFAULT_INCLUDE_OPTIONS = { signature: :SHA1, compression: :GZIP } DEFAULT_INCLUDE_FILE_LIST = ['/'] # Constructs an array where each element is a line for the Fileset's bacula config # # @return [Array] def to_bacula_config_array ['FileSet {'] + [" Name = \"#{name_for_config}\""] + include_directions_to_config_array + exclude_directions_to_config_array + ['}'] end # Provides a human readable projection of the fileset # # @return [String] def human_readable result = "Directories:\n" result << "\t* " << include_directions['file'].join("\n\t* ") if exclude_directions.present? result << "\n\nExcluded:\n" result << "\t* " << exclude_directions.join("\n\t*") end result end # Generates a name that will be used for the configuration file. # It is the name that will be sent to Bacula through the configuration # files. # # @return [String] def name_for_config [host.name, name].join(' ') end # Returns the hosts that have enabled jobs that use this fileset # # @return [ActiveRecord::Relation] the participating hosts def participating_hosts Host.joins(:job_templates).where(job_templates: { enabled: true, fileset_id: id }).uniq end # Creates a default fileset resource for a simple config def default_resource(name, time_hex, opts = {}) @include_files = opts[:files].presence || DEFAULT_INCLUDE_FILE_LIST self.name = "files_#{name}_#{time_hex}" self.exclude_directions = DEFAULT_EXCLUDED save! self end private def has_included_files if include_files.blank? || include_files.all?(&:blank?) errors.add(:include_files, :cant_be_blank) end end def sanitize_include_directions files = include_files.compact.uniq.keep_if(&:present?) rescue nil - return false if files.blank? + if files.blank? + self.errors[:include_files] << "Include files can't be empty" + return false + end files = files.map {|file| Shellwords.escape(file) } self.include_directions = { options: DEFAULT_INCLUDE_OPTIONS, file: files } end def sanitize_exclude_directions self.exclude_directions = begin exclude_directions.keep_if(&:present?).uniq.map do |x| Shellwords.escape(x) end rescue nil end end def exclude_directions_to_config_array return [] if exclude_directions.empty? [' Exclude {'] + exclude_directions.map { |x| " File = \"#{x}\"" } + [' }'] end def include_directions_to_config_array return [] if include_directions.blank? [" Include {"] + included_options + included_files + [' }'] end def included_options formatted = [" Options {"] options = include_directions.deep_symbolize_keys[:options]. reverse_merge(DEFAULT_INCLUDE_OPTIONS) options.each do |k,v| if not [:wildfile].include? k formatted << " #{k} = #{v}" else formatted << v.map { |f| " #{k} = \"#{f}\"" } end end formatted << " }" formatted end def included_files include_directions['file'].map { |f| " File = #{f}" } end def included_wildfile include_directions['wildfile'].map { |f| " wildfile = \"#{f}\"" }.join("\n") end end diff --git a/app/views/filesets/_form.html.erb b/app/views/filesets/_form.html.erb index 78c7f5f..5a28390 100644 --- a/app/views/filesets/_form.html.erb +++ b/app/views/filesets/_form.html.erb @@ -1,59 +1,58 @@ <%= bootstrap_form_for(@fileset, url: url, method: method, layout: :horizontal, label_col: 'col-xs-3', control_col: 'col-xs-8') do |f| %>