s3ranger 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +49 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/README.txt +406 -0
- data/README_s3cmd.txt +172 -0
- data/Rakefile +1 -0
- data/bin/s3ranger +105 -0
- data/lib/s3ranger/cmd.rb +112 -0
- data/lib/s3ranger/commands.rb +114 -0
- data/lib/s3ranger/config.rb +43 -0
- data/lib/s3ranger/exceptions.rb +38 -0
- data/lib/s3ranger/sync.rb +338 -0
- data/lib/s3ranger/util.rb +5 -0
- data/lib/s3ranger/version.rb +3 -0
- data/lib/s3ranger.rb +2 -0
- data/s3config.yml.example +3 -0
- data/s3ranger.gemspec +31 -0
- data/spec/fixtures/nodes/1.txt +1 -0
- data/spec/local_source_spec.rb +55 -0
- data/spec/main_spec.rb +141 -0
- data/spec/spec_helper.rb +25 -0
- metadata +177 -0
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/s3ranger
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
|
|
4
|
+
|
|
5
|
+
require "s3ranger/exceptions"
|
|
6
|
+
require "s3ranger/config"
|
|
7
|
+
require "s3ranger/cmd"
|
|
8
|
+
|
|
9
|
+
conf = S3Ranger::Config.new
|
|
10
|
+
|
|
11
|
+
# Time to load config and see if we've got everything we need to cook our salad
|
|
12
|
+
begin
|
|
13
|
+
conf.read
|
|
14
|
+
rescue S3Ranger::NoConfigFound => exc
|
|
15
|
+
# We can't proceed without having those two vars set
|
|
16
|
+
if not (conf.has_key? "AWS_ACCESS_KEY_ID" and conf.has_key? "AWS_SECRET_ACCESS_KEY")
|
|
17
|
+
$stderr.puts "You didn't set up your environment variables :("
|
|
18
|
+
$stderr.puts "I tried the following paths:"
|
|
19
|
+
exc.paths_checked.each {|path| $stderr.puts " * #{path}/s3config.yml"}
|
|
20
|
+
|
|
21
|
+
$stderr.puts "You could try to set the `S3CONF' environment variable."
|
|
22
|
+
$stderr.puts "Learn how to do that here: https://github.com/clarete/s3ranger"
|
|
23
|
+
exit
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Step aside, the star of this show is here. Let's try to create the
|
|
29
|
+
# environment to run the requested command. And feed the user back if
|
|
30
|
+
# information needed was not enough
|
|
31
|
+
begin
|
|
32
|
+
S3Ranger::Cmd.new(conf)
|
|
33
|
+
|
|
34
|
+
rescue S3Ranger::FailureFeedback => exc
|
|
35
|
+
$stderr.puts exc.message
|
|
36
|
+
exit 1
|
|
37
|
+
|
|
38
|
+
rescue S3Ranger::WrongUsage => exc
|
|
39
|
+
name = $0.split('/').last
|
|
40
|
+
|
|
41
|
+
$stderr.puts <<"ENDUSAGE"
|
|
42
|
+
Usage: #{name} [options] <command> [arg(s)]
|
|
43
|
+
|
|
44
|
+
Global Options:
|
|
45
|
+
-h, --help
|
|
46
|
+
-v, --verbose
|
|
47
|
+
-n, --dryrun
|
|
48
|
+
-d, --debug
|
|
49
|
+
--progress
|
|
50
|
+
|
|
51
|
+
#{name} listbuckets
|
|
52
|
+
List all available buckets
|
|
53
|
+
|
|
54
|
+
#{name} createbucket <bucket>
|
|
55
|
+
Creates a new bucket
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
|
|
59
|
+
-a <ACL>, --acl=(private|public_read|public_read_write)
|
|
60
|
+
|
|
61
|
+
#{name} deletebucket [options] <bucket>
|
|
62
|
+
Removes an existing bucket
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
-f, --force Deletes non-empty buckets (BE CAREFUL)
|
|
66
|
+
|
|
67
|
+
#{name} list <bucket>[:prefix]
|
|
68
|
+
List content inside of bucket
|
|
69
|
+
|
|
70
|
+
if `prefix' is present, only content under `prefix' will be listed.
|
|
71
|
+
|
|
72
|
+
#{name} delete <bucket>:<key>
|
|
73
|
+
Removes `key` from `bucket`
|
|
74
|
+
|
|
75
|
+
#{name} url [options] <bucket>:<key>
|
|
76
|
+
Generates a presigned URL for an operation on the object named `key' found on
|
|
77
|
+
`bucket'.
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--no-ssl
|
|
81
|
+
--expires-in=(<# of seconds> | [#d|#h|#m|#s])
|
|
82
|
+
|
|
83
|
+
#{name} get <bucket>:<key> <file>
|
|
84
|
+
Retrieves the remote `key` object from `bucket` and saves to the local path
|
|
85
|
+
specified in `file`
|
|
86
|
+
|
|
87
|
+
#{name} put <bucket>[:<key>] <file>
|
|
88
|
+
Uploads the file `file` to the `bucket` under the path `key`
|
|
89
|
+
|
|
90
|
+
#{name} sync <source> <destination>
|
|
91
|
+
|
|
92
|
+
One of source or destination must be remote and the other must be local,
|
|
93
|
+
where local points to a folder in the file system and remote conform to the
|
|
94
|
+
format `<bucket>[:<key>]`.
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
-e <pattern>, --exclude=<pattern>
|
|
98
|
+
-k, --keep
|
|
99
|
+
-d, --dry-run
|
|
100
|
+
|
|
101
|
+
ENDUSAGE
|
|
102
|
+
|
|
103
|
+
$stderr.puts "\nCurrent error:\n #{exc.msg}\n" if exc.msg
|
|
104
|
+
exit exc.error_code
|
|
105
|
+
end
|
data/lib/s3ranger/cmd.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# (c) 2013 Lincoln de Sousa <lincoln@clarete.li>
|
|
2
|
+
# (c) 2007 s3sync.net
|
|
3
|
+
#
|
|
4
|
+
# This software code is made available "AS IS" without warranties of any
|
|
5
|
+
# kind. You may copy, display, modify and redistribute the software
|
|
6
|
+
# code either by itself or as incorporated into your code; provided that
|
|
7
|
+
# you do not remove any proprietary notices. Your use of this software
|
|
8
|
+
# code is at your own risk and you waive any claim against the author
|
|
9
|
+
# with respect to your use of this software code.
|
|
10
|
+
|
|
11
|
+
require 'getoptlong'
|
|
12
|
+
require 's3ranger/commands'
|
|
13
|
+
require 's3ranger/util'
|
|
14
|
+
|
|
15
|
+
module S3Ranger
|
|
16
|
+
|
|
17
|
+
class Cmd
|
|
18
|
+
|
|
19
|
+
def initialize(conf = conf)
|
|
20
|
+
# The chain that initializes our command and find the right action
|
|
21
|
+
options, command, bucket, key, file = read_info_from_args(parse_args())
|
|
22
|
+
|
|
23
|
+
# Finding the right command to run
|
|
24
|
+
(cmd = find_cmd(command)) || (raise WrongUsage.new(nil, "Command `#{command}' does not exist"))
|
|
25
|
+
|
|
26
|
+
# Now that we're sure we have things to do, we need to connect to amazon
|
|
27
|
+
s3 = AWS::S3.new(
|
|
28
|
+
:access_key_id => conf[:AWS_ACCESS_KEY_ID],
|
|
29
|
+
:secret_access_key => conf[:AWS_SECRET_ACCESS_KEY],
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Calling the actuall command
|
|
33
|
+
cmd.call({
|
|
34
|
+
:options => options,
|
|
35
|
+
:s3 => s3,
|
|
36
|
+
:bucket => bucket,
|
|
37
|
+
:key => key,
|
|
38
|
+
:file => file,
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_cmd name
|
|
43
|
+
sym = "_cmd_#{name}".to_sym
|
|
44
|
+
return nil unless Commands.public_methods.include? sym
|
|
45
|
+
return Commands.method sym
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_args
|
|
49
|
+
options = Hash.new
|
|
50
|
+
|
|
51
|
+
args = [
|
|
52
|
+
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
|
53
|
+
['--force', '-f', GetoptLong::NO_ARGUMENT],
|
|
54
|
+
['--acl', '-a', GetoptLong::REQUIRED_ARGUMENT],
|
|
55
|
+
['--method', '-m', GetoptLong::REQUIRED_ARGUMENT],
|
|
56
|
+
['--exclude', '-e', GetoptLong::REQUIRED_ARGUMENT],
|
|
57
|
+
['--keep', '-k', GetoptLong::NO_ARGUMENT],
|
|
58
|
+
['--dry-run', '-d', GetoptLong::NO_ARGUMENT],
|
|
59
|
+
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
|
|
60
|
+
['--no-ssl', GetoptLong::NO_ARGUMENT],
|
|
61
|
+
['--expires-in', GetoptLong::REQUIRED_ARGUMENT],
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
GetoptLong.new(*args).each {|opt, arg| options[opt] = (arg || true)}
|
|
66
|
+
rescue StandardError => exc
|
|
67
|
+
raise WrongUsage.new nil, exc.message
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Let's just show the help to the user
|
|
71
|
+
raise WrongUsage.new(0, nil) if options['--help']
|
|
72
|
+
|
|
73
|
+
# Returning the options to the next level
|
|
74
|
+
options
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def read_info_from_args(options)
|
|
78
|
+
# Parsing expre date
|
|
79
|
+
if options['--expires-in'] =~ /d|h|m|s/
|
|
80
|
+
|
|
81
|
+
val = 0
|
|
82
|
+
|
|
83
|
+
options['--expires-in'].scan(/(\d+\w)/) do |track|
|
|
84
|
+
_, num, what = /(\d+)(\w)/.match(track[0]).to_a
|
|
85
|
+
num = num.to_i
|
|
86
|
+
|
|
87
|
+
case what
|
|
88
|
+
when "d"; val += num * 86400
|
|
89
|
+
when "h"; val += num * 3600
|
|
90
|
+
when "m"; val += num * 60
|
|
91
|
+
when "s"; val += num
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
options['--expires-in'] = val
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reading what to do from the user
|
|
99
|
+
command = ARGV.shift
|
|
100
|
+
raise WrongUsage.new(nil, "Need a command (eg.: `list', `listbuckets', etc)") if not command
|
|
101
|
+
|
|
102
|
+
key, file = ARGV
|
|
103
|
+
|
|
104
|
+
# Parsing the bucket name
|
|
105
|
+
bucket = nil
|
|
106
|
+
bucket, key = key.split(':') if key
|
|
107
|
+
|
|
108
|
+
# Returning things we need in the next level
|
|
109
|
+
[options, command, bucket, key, file]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
require 's3ranger/exceptions'
|
|
2
|
+
require 's3ranger/sync'
|
|
3
|
+
require 'aws/s3'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
module Commands
|
|
7
|
+
|
|
8
|
+
include S3Ranger
|
|
9
|
+
|
|
10
|
+
AVAILABLE_ACLS = [:public_read, :public_read_write, :private]
|
|
11
|
+
|
|
12
|
+
AVAILABLE_METHODS = ['read', 'get', 'put', 'write', 'delete']
|
|
13
|
+
|
|
14
|
+
def Commands._cmd_listbuckets args
|
|
15
|
+
args[:s3].buckets.each do |bkt|
|
|
16
|
+
puts "#{bkt.name}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def Commands._cmd_createbucket args
|
|
21
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
params = {}
|
|
25
|
+
if acl = args[:options]['--acl']
|
|
26
|
+
raise WrongUsage.new(nil, "Invalid ACL. Should be any of #{EXISTING_ACLS.join ', '}") if not AVAILABLE_ACLS.include? acl
|
|
27
|
+
params.merge!({:acl => acl.to_sym})
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
args[:s3].buckets.create args[:bucket], params
|
|
31
|
+
rescue AWS::S3::Errors::BucketAlreadyExists => exc
|
|
32
|
+
raise FailureFeedback.new("Bucket `#{bucket}' already exists")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def Commands._cmd_deletebucket args
|
|
37
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
38
|
+
|
|
39
|
+
# Getting the bucket
|
|
40
|
+
bucket_obj = args[:s3].buckets[args[:bucket]]
|
|
41
|
+
|
|
42
|
+
# Do not kill buckets with content unless explicitly asked
|
|
43
|
+
if not args[:options]['--force'] and bucket_obj.objects.count > 0
|
|
44
|
+
raise FailureFeedback.new("Cowardly refusing to remove non-empty bucket `#{bucket}'. Try with -f.")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
bucket_obj.delete!
|
|
49
|
+
rescue AWS::S3::Errors::AccessDenied => exc
|
|
50
|
+
raise FailureFeedback.new("Access Denied")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def Commands._cmd_list args
|
|
55
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
56
|
+
args[:s3].buckets[args[:bucket]].objects.with_prefix(args[:key] || "").each do |object|
|
|
57
|
+
puts "#{object.key}\t#{object.content_length}\t#{object.last_modified}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def Commands._cmd_delete args
|
|
62
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
63
|
+
raise WrongUsage.new(nil, "You need to inform a key") if not args[:key]
|
|
64
|
+
args[:s3].buckets[args[:bucket]].objects[args[:key]].delete
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def Commands._cmd_url args
|
|
68
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
69
|
+
raise WrongUsage.new(nil, "You need to inform a key") if not args[:key]
|
|
70
|
+
|
|
71
|
+
method = args[:options]['--method'] || 'read'
|
|
72
|
+
raise WrongUsage.new(nil, "") unless AVAILABLE_METHODS.include? method
|
|
73
|
+
|
|
74
|
+
opts = {}
|
|
75
|
+
opts.merge!({:secure => args[:options]["--no-ssl"].nil?})
|
|
76
|
+
opts.merge!({:expires => args[:options]["--expires-in"]}) if args[:options]["--expires-in"]
|
|
77
|
+
p (args[:s3].buckets[args[:bucket]].objects[args[:key]].url_for method.to_sym, opts).to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def Commands._cmd_put args
|
|
81
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
82
|
+
raise WrongUsage.new(nil, "You need to inform a file") if not args[:file]
|
|
83
|
+
|
|
84
|
+
# key + file name
|
|
85
|
+
name = S3Ranger.safe_join [args[:key], File.basename(args[:file])]
|
|
86
|
+
args[:s3].buckets[args[:bucket]].objects[name].write Pathname.new(args[:file])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def Commands._cmd_get args
|
|
90
|
+
raise WrongUsage.new(nil, "You need to inform a bucket") if not args[:bucket]
|
|
91
|
+
raise WrongUsage.new(nil, "You need to inform a key") if not args[:key]
|
|
92
|
+
raise WrongUsage.new(nil, "You need to inform a file") if not args[:file]
|
|
93
|
+
|
|
94
|
+
# Saving the content to be downloaded to the current directory if the
|
|
95
|
+
# destination is a directory
|
|
96
|
+
path = File.absolute_path args[:file]
|
|
97
|
+
path = S3Ranger.safe_join [path, File.basename(args[:key])] if File.directory? path
|
|
98
|
+
|
|
99
|
+
File.open(path, 'wb') do |f|
|
|
100
|
+
begin
|
|
101
|
+
args[:s3].buckets[args[:bucket]].objects[args[:key]].read do |chunk| f.write(chunk) end
|
|
102
|
+
rescue AWS::S3::Errors::NoSuchBucket
|
|
103
|
+
raise FailureFeedback.new("There's no bucket named `#{args[:bucket]}'")
|
|
104
|
+
rescue AWS::S3::Errors::NoSuchKey
|
|
105
|
+
raise FailureFeedback.new("There's no key named `#{args[:key]}' in the bucket `#{args[:bucket]}'")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def Commands._cmd_sync args
|
|
111
|
+
cmd = SyncCommand.new args, *ARGV
|
|
112
|
+
cmd.run
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# This software code is made available "AS IS" without warranties of any
|
|
2
|
+
# kind. You may copy, display, modify and redistribute the software
|
|
3
|
+
# code either by itself or as incorporated into your code; provided that
|
|
4
|
+
# you do not remove any proprietary notices. Your use of this software
|
|
5
|
+
# code is at your own risk and you waive any claim against the author
|
|
6
|
+
# with respect to your use of this software code.
|
|
7
|
+
# (c) 2007 alastair brunton
|
|
8
|
+
#
|
|
9
|
+
# modified to search out the yaml in several places, thanks wkharold.
|
|
10
|
+
|
|
11
|
+
require 'yaml'
|
|
12
|
+
require 's3ranger/exceptions'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
module S3Ranger
|
|
16
|
+
|
|
17
|
+
class Config < Hash
|
|
18
|
+
def read
|
|
19
|
+
paths_checked = []
|
|
20
|
+
|
|
21
|
+
["#{ENV['S3CONF']}", "#{ENV['HOME']}/.s3conf", "/etc/s3conf"].each do |path|
|
|
22
|
+
|
|
23
|
+
# Filtering some garbage
|
|
24
|
+
next if path.nil? or path.strip.empty?
|
|
25
|
+
|
|
26
|
+
# Feeding the user feedback in case of failure
|
|
27
|
+
paths_checked << path
|
|
28
|
+
|
|
29
|
+
# Time for the dirty work, let's parse the config file and feed our
|
|
30
|
+
# internal hash
|
|
31
|
+
if File.exists?("#{path}/s3config.yml")
|
|
32
|
+
config = YAML.load_file("#{path}/s3config.yml")
|
|
33
|
+
config.each_pair do |key, value|
|
|
34
|
+
self[key.upcase.to_sym] = value
|
|
35
|
+
end
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
raise NoConfigFound.new paths_checked
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# (c) 2013 Lincoln de Sousa <lincoln@clarete.li>
|
|
2
|
+
#
|
|
3
|
+
# This software code is made available "AS IS" without warranties of any
|
|
4
|
+
# kind. You may copy, display, modify and redistribute the software
|
|
5
|
+
# code either by itself or as incorporated into your code; provided that
|
|
6
|
+
# you do not remove any proprietary notices. Your use of this software
|
|
7
|
+
# code is at your own risk and you waive any claim against the author
|
|
8
|
+
# with respect to your use of this software code.
|
|
9
|
+
|
|
10
|
+
module S3Ranger
|
|
11
|
+
|
|
12
|
+
class SyncException < StandardError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class NoConfigFound < SyncException
|
|
16
|
+
|
|
17
|
+
attr_accessor :paths_checked
|
|
18
|
+
|
|
19
|
+
def initialize(paths_checked)
|
|
20
|
+
@paths_checked = paths_checked
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class WrongUsage < SyncException
|
|
25
|
+
|
|
26
|
+
attr_accessor :error_code
|
|
27
|
+
attr_accessor :msg
|
|
28
|
+
|
|
29
|
+
def initialize(error_code, msg)
|
|
30
|
+
@error_code = error_code || 1
|
|
31
|
+
@msg = msg
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class FailureFeedback < SyncException
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
end
|