lib/s3ranger/cli.rb in s3ranger-0.2.1 vs lib/s3ranger/cli.rb in s3ranger-0.3.0

- old
+ new

@@ -1,5 +1,29 @@ +# s3ranger - Tool belt for managing your S3 buckets +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Lincoln de Sousa <lincoln@clarete.li> +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + require 's3ranger/version' require 's3ranger/exceptions' require 's3ranger/sync' require 'aws/s3' require 'cmdparse' @@ -10,11 +34,39 @@ AVAILABLE_ACLS = [:public_read, :public_read_write, :private] AVAILABLE_METHODS = ['read', 'get', 'put', 'write', 'delete'] - class ListBuckets < CmdParse::Command + class BaseCmd < CmdParse::Command + + @has_prefix = false + + def has_options? + not options.instance_variables.empty? + end + + def has_prefix? + @has_prefix + end + + def usage + u = [] + u << "Usage: #{File.basename commandparser.program_name} #{name} " + u << "[options] " if has_options? + u << "bucket" if has_args? + + if has_prefix? == 'required' + u << ':prefix' + elsif has_prefix? + u << "[:prefix]" + end + + u.join '' + end + end + + class ListBuckets < BaseCmd def initialize super 'listbuckets', false, false, false @short_desc = "List all available buckets for your user" end @@ -24,11 +76,11 @@ puts "#{bkt.name}" end end end - class CreateBucket < CmdParse::Command + class CreateBucket < BaseCmd attr_accessor :acl def initialize super 'createbucket', false, false @@ -58,11 +110,11 @@ raise FailureFeedback.new("Bucket `#{bucket}' already exists") end end end - class DeleteBucket < CmdParse::Command + class DeleteBucket < BaseCmd attr_accessor :force def initialize super 'deletebucket', false, false @@ -90,24 +142,32 @@ bucket_obj.delete! end end - class List < CmdParse::Command + class List < BaseCmd attr_accessor :max_entries def initialize super 'list', false, false @short_desc = "List items filed under a given bucket" @max_entries = 0 + @delimiter = "\t" + + @has_prefix = true + self.options = CmdParse::OptionParserWrapper.new do |opt| opt.on("-m", "--max-entries=NUM", "Limit the number of entries to output") {|m| @max_entries = m } + + opt.on("-d", "--delimiter=D", "Charactere used to separate columns") {|d| + @delimiter = d + } end end def run s3, bucket, key, file, args raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket @@ -117,46 +177,61 @@ if @max_entries > 0 collection = collection.page(:per_page => max = @max_entries) end collection.each {|object| - puts "#{object.key}\t#{object.content_length}\t#{object.last_modified}" + o = [] + o << object.key + o << @delimiter + o << object.content_length + o << @delimiter + o << object.last_modified + puts o.join } end end - class Delete < CmdParse::Command + class Delete < BaseCmd def initialize - super 'delete', false, false, false + super 'delete', false, false @short_desc = "Delete a key from a bucket" + + @has_prefix = 'required' end def run s3, bucket, key, file, args raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket raise WrongUsage.new(nil, "You need to inform a key") if not key s3.buckets[bucket].objects[key].delete end end - class Url < CmdParse::Command + class Url < BaseCmd attr_accessor :method attr_accessor :secure def initialize super 'url', false, false - @short_desc = "Generates a url pointing to the given key" - @method = 'read' + @short_desc = "Generates public urls or authenticated endpoints for the object" + @description = "Notice that --method and --public are mutually exclusive" + @method = false + @public = false @secure = true @expires_in = false + @has_prefix = 'required' self.options = CmdParse::OptionParserWrapper.new do |opt| - opt.on("-m", "--method", "Options: #{AVAILABLE_METHODS.join ', '}") {|m| + opt.on("-m", "--method=METHOD", "Options: #{AVAILABLE_METHODS.join ', '}") {|m| @method = m } + opt.on("-p", "--public", "Generates a public (not authenticated) URL for the object") {|p| + @public = p + } + opt.on("--no-ssl", "Generate an HTTP link, no HTTPS") { @secure = false } opt.on("--expires-in=EXPR", "How long the link takes to expire. Format: <# of seconds> | [#d|#h|#m|#s]") { |expr| @@ -178,24 +253,33 @@ end def run s3, bucket, key, file, args raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket raise WrongUsage.new(nil, "You need to inform a key") if not key - raise WrongUsage.new(nil, "Unknown method #{@method}") unless AVAILABLE_METHODS.include? @method + raise WrongUsage.new(nil, "Params --method and --public are mutually exclusive") if (@method and @public) + if not AVAILABLE_METHODS.include? method = @method || 'read' + raise WrongUsage.new(nil, "Unknown method #{method}") + end opts = {} opts.merge!({:secure => @secure}) - opts.merge!({:expires => @expires_in}) if @expires_in - puts (s3.buckets[bucket].objects[key].url_for @method.to_sym, opts).to_s + + if @public + puts (s3.buckets[bucket].objects[key].public_url opts).to_s + else + opts.merge!({:expires => @expires_in}) if @expires_in + puts (s3.buckets[bucket].objects[key].url_for method.to_sym, opts).to_s + end end end - class Put < CmdParse::Command + class Put < BaseCmd def initialize super 'put', false, false @short_desc = 'Upload a file to a bucket under a certain prefix' + @has_prefix = true end def run s3, bucket, key, file, args raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket raise WrongUsage.new(nil, "You need to inform a file") if not file @@ -203,14 +287,15 @@ name = S3Ranger.safe_join [key, File.basename(file)] s3.buckets[bucket].objects[name].write Pathname.new(file) end end - class Get < CmdParse::Command + class Get < BaseCmd def initialize super 'get', false, false @short_desc = "Retrieve an object and save to the specified file" + @has_prefix = 'required' end def run s3, bucket, key, file, args raise WrongUsage.new(nil, "You need to inform a bucket") if not bucket raise WrongUsage.new(nil, "You need to inform a key") if not key @@ -224,11 +309,11 @@ s3.buckets[bucket].objects[key].read do |chunk| f.write(chunk) end end end end - class Sync < CmdParse::Command + class Sync < BaseCmd attr_accessor :s3 attr_accessor :exclude attr_accessor :keep attr_accessor :dry_run attr_accessor :verbose @@ -242,11 +327,11 @@ @keep = false @dry_run = false @verbose = false self.options = CmdParse::OptionParserWrapper.new do |opt| - opt.on("-x EXPR", "--exclude=EXPR", "") {|v| + opt.on("-x EXPR", "--exclude=EXPR", "Skip copying files that matches this pattern. (Ruby REs)") {|v| @exclude = v } opt.on("-k", "--keep", "Keep files even if they don't exist in source") { @keep = true @@ -261,33 +346,61 @@ @verbose = true } end end + def usage + "Usage: #{File.basename commandparser.program_name} #{name} source destination" + end + + def description + @description =<<END.strip + +Where `source' and `description' might be either local or remote +addresses. A local address is simply a path in your local file +system. e.g: + + /tmp/notes.txt + +A remote address is a combination of the `bucket` name and +an optional `prefix`: + + disc.company.com:reports/2013/08/30.html + +So, a full example would be something like this + + $ #{File.basename commandparser.program_name} sync Work/reports disc.company.com:reports/2013/08 + +The above line will update the remote folder `reports/2013/08` with the +contents of the local folder `Work/reports`. +END + end + def run s3, bucket, key, file, args @s3 = s3 cmd = SyncCommand.new self, *args cmd.run end end def run conf cmd = CmdParse::CommandParser.new true + cmd.program_name = File.basename $0 cmd.program_version = S3Ranger::VERSION cmd.options = CmdParse::OptionParserWrapper.new do |opt| opt.separator "Global options:" end cmd.main_command.short_desc = 'Tool belt for managing your S3 buckets' - cmd.main_command.description = [] \ - << "Below you have a list of commands will allow you to manage your content" \ - << "stored in S3 buckets. For more information on each command, you can always" \ - << "use the `--help' parameter, just like this:" \ - << "" \ - << " $ #{$0} sync --help" \ + cmd.main_command.description =<<END.strip +S3Ranger provides a list of commands that will allow you to manage your content +stored in S3 buckets. To learn about each feature, please use the `help` +command: + $ #{File.basename $0} help sync" +END # Commands used more often cmd.add_command List.new cmd.add_command Delete.new cmd.add_command Url.new cmd.add_command Put.new @@ -297,11 +410,10 @@ # Bucket related options cmd.add_command ListBuckets.new cmd.add_command CreateBucket.new cmd.add_command DeleteBucket.new - # Built-in commands cmd.add_command CmdParse::HelpCommand.new cmd.add_command CmdParse::VersionCommand.new # Defining the `execute` method as a closure, so we can forward the @@ -330,9 +442,11 @@ raise FailureFeedback.new("Access Denied") rescue AWS::S3::Errors::NoSuchBucket raise FailureFeedback.new("There's no bucket named `#{bucket}'") rescue AWS::S3::Errors::NoSuchKey raise FailureFeedback.new("There's no key named `#{key}' in the bucket `#{bucket}'") + rescue AWS::S3::Errors::Base => exc + raise FailureFeedback.new("Error: `#{exc.message}'") end } end cmd.parse