# Magnoline, Command line tool for the magnolia CMS #Copyright (C) 2007 Nicolas Modrzyk # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # #This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # # Nicolas Modrzyk hellonico at gmail dot com # # Things learn in this code: # 1. Command line parsing # 2. Multipart HTTP POST require 'rubygems' require 'cooloptions' require 'net/http' require 'ping' require 'http-access2' require 'yaml' include Net # reopening the session manager class to specify different timeouts settings class HTTPAccess2::SessionManager def initialize @proxy = nil @agent_name = nil @from = nil @protocol_version = nil @debug_dev = nil @socket_sync = true @chunk_size = 8192 @connect_timeout = 60 @connect_retry = 0 @send_timeout = 120 @receive_timeout = 300 # For each read_block_size bytes @read_block_size = 8192 @ssl_config = nil @sess_pool = [] @sess_pool_mutex = Mutex.new end end # # Simple client to import/export content of a magnolia repository. # This is using the import/export servlet accessible in magnolia module MagnoliaClient # # Parse the arguments to instanciate the proper ImportExport object class ImportExportSetUp CMD_IMPORT = "import" CMD_EXPORT = "export" IMPORT_BEHAVIORS = [:new,:remove,:replace] # # this method does the parsing of the arguments. Validates them # return the ImportExport object def parse(argv) options = CoolOptions.parse!('[options]') do |opt| opt.on 'action STRING','one of import/export' opt.on 'out STRING','path to directory where to put the content of the answer', "" opt.on 'repository-path STRING','path on the repository to take into account', "/help" opt.on 'workspace STRING',' the repository/workspace to take into account', "website" opt.on 'user STRING','user name to authenticate with', 'superuser' opt.on 'password STRING','password', 'superuser' opt.on 'f)import-file STRING','path to the xml file to import', '' opt.on 'server-url STRING','base url of the magnolia service', 'http://localhost:8080/magnoliaAuthor' opt.on 'behavior STRING', 'import behavior. One of new/remove/replace or 0/1/2', "0" opt.on 'verbose', 'give some more processing info on the command line', false opt.on 'console', 'output to standard output, bypass the out option', false opt.on 'buffer', 'Use a buffer to stream the output from the server', false opt.on 'z)batch-file STRING','start a batch execution of actions from a file. Path to that file', nil opt.after do |r| r.out = File.expand_path(r.out) opt.error("Invalid action:"+r.action) unless (r.action == CMD_IMPORT || r.action == CMD_EXPORT) uri = URI.parse(r.server_url) opt.error("host is not reachable:" + host) unless Ping.pingecho(uri.host,10,uri.port) r.behavior = parse_behavior(r.behavior) if r.action == CMD_IMPORT end end return get_commands(options) end # # return the array of commands properly initialized def get_commands(options) commands = Array.new if !options.batch_file commands << get_action(options) else open(File.expand_path(options.batch_file)) do |file| file.each do |l| if not l[0] == '#' opts = l.split(/\s+/) options.workspace = opts[0] options.repository_path = opts[1] options.import_file = opts[2] if options.action == CMD_IMPORT commands << get_action(options) else # skip the line. This is a comment end end end commands end end def get_action(options) return Export.new(options) if options.action == CMD_EXPORT return Import.new(options) if options.action == CMD_IMPORT end # # Parse the import behavior, from an integer or a string # Do this while parsing to get proper error output when needed def parse_behavior(value) IMPORT_BEHAVIORS.each_with_index { |behavior, index| return index if value.to_i == index return index if value.to_s == behavior.to_s } raise "Invalid behavior:#{value}" end end # # Class that does the actual import export class ImportExportBase MAGNOLIA_BASE_URL = "/.magnolia/pages/" MAGNOLIA_IMPORT_URL = MAGNOLIA_BASE_URL + "import.html" MAGNOLIA_EXPORT_URL = MAGNOLIA_BASE_URL + "export.html" # # No checking of options, this is done in the parser # assign values from the hash to attributes def initialize(options) @console = options.console @action = options.action @server_url = options.server_url @repository_path = options.repository_path @workspace = options.workspace @user = options.user @password = options.password @verbose = options.verbose @options = options @out = outfile_path @buffer = options.buffer end # # compute the outfile path # take special care when the repository_path is the root def outfile_path subr = if @repository_path == '/' '' else @repository_path.gsub('/','.') end File.expand_path(@options.out+'/'+@workspace+subr+'.xml') end # # prepare form data. Should return a hash with the needed parameters # this has to be implemented in the subclasses def prepare_form_data raise "Sub classing class need to implement this method" end # # trying to fix a bug in http-access2 where all the parameters values are loosing their last char # can't find a reason for that. def pad_data(data) data.each { |key,value| begin if not key==:mgnlFileImport value = value + " "; data[key] = value.to_s end rescue end } end # # main method # this create the necessary http client to simulate interaction and just post the # data collected from subclasses to the form. def exec client = HTTPAccess2::Client.new boundary = Array::new(8){ "%2.2d" % rand(42) }.join('__') extheader = {'content-type' => "multipart/form-data; boundary=___#{ boundary }___"} client.set_basic_auth(@server_url, @user, @password) form_data = pad_data(prepare_form_data) verbose(form_data) if @verbose if @buffer process_with_buffer(client, form_data, extheader) else process_with_no_buffer(client, form_data, extheader) end end # # use a simple variable to process the response from the server def process_with_no_buffer(client, form_data, extheader) res = client.post_content(@full_url, form_data, extheader) if @console puts res else f = File.open(@out,'w') f.puts res f.flush f.close puts "Output of action "+@action+" has been written to "+@out if @verbose end end # # use ruby blocks to process the response from the server def process_with_buffer(client, form_data, extheader) if @console client.post_content(@full_url, form_data, extheader) do |res| res.each('') do |line| puts line end end else f = File.open(@out,'w') client.post_content(@full_url, form_data, extheader) do |res| res.each do |line| f << line f.flush end end f.flush f.close puts "Output of action "+@action+" has been written to "+@out if @verbose end end # # Verbose output before action def verbose(form_data) puts "------------ \tOptions data\t ---------------" puts @options.to_yaml puts "------------ \tPost data\t\t ---------------" puts form_data.to_yaml puts "------------ \tEnd data\t\t ---------------" end end # # Class responsible for import specific code class Import < ImportExportBase def initialize(options) super(options) @action = "importxml " @full_url = @server_url + MAGNOLIA_IMPORT_URL @import_file = options.import_file end def prepare_form_data form = { :mgnlFileImport=>open(@import_file), :mgnlRepository=>@workspace, :mgnlPath=>@repository_path, :mgnlKeepVersions=>false, :mgnlFormat=>false, :command=>@action } end end # # Class responsible for export specific code class Export < ImportExportBase def initialize(options) super(options) @action = "exportxml" @full_url = @server_url + MAGNOLIA_EXPORT_URL end def prepare_form_data post_data = { :mgnlRepository=>@workspace, :mgnlPath=>@repository_path, :command=>@action, #:mgnlKeepVersions=>false, #:mgnlFormat=>false, :ext=>'.xml' } end end end