sdbtools 0.0.1
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/Rakefile +48 -0
- data/VERSION +1 -0
- data/bin/sdbtool +353 -0
- data/lib/sdbtools.rb +348 -0
- metadata +80 -0
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "sdbtools"
|
8
|
+
gem.summary = %Q{A high-level OO interface to Amazon SimpleDB}
|
9
|
+
gem.description = <<END
|
10
|
+
SDBTools layers a higher-level OO interface on top of RightAWS, as well as
|
11
|
+
providing some command-line utilities for working with SimpleDB.
|
12
|
+
END
|
13
|
+
gem.email = "devs@devver.net"
|
14
|
+
gem.homepage = "http://github.com/devver/sdbtools"
|
15
|
+
gem.authors = ["Avdi Grimm"]
|
16
|
+
gem.add_dependency "right_aws", "~> 1.10"
|
17
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
43
|
+
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
45
|
+
rdoc.title = "sdbtools #{version}"
|
46
|
+
rdoc.rdoc_files.include('README*')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/bin/sdbtool
ADDED
@@ -0,0 +1,353 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'main', '~> 4.2'
|
4
|
+
gem 'fattr', '~> 2.1'
|
5
|
+
gem 'highline', '~> 1.5'
|
6
|
+
gem 'right_aws', '~> 1.10'
|
7
|
+
gem 'progressbar', '~> 0.0.3'
|
8
|
+
gem 'libxml-ruby', '~> 1.1'
|
9
|
+
|
10
|
+
require 'main'
|
11
|
+
require 'highline/import'
|
12
|
+
require 'fattr'
|
13
|
+
require 'right_aws'
|
14
|
+
require 'progressbar'
|
15
|
+
require 'pstore'
|
16
|
+
require 'pathname'
|
17
|
+
require 'fileutils'
|
18
|
+
require 'yaml'
|
19
|
+
|
20
|
+
require File.expand_path('../lib/sdbtools', File.dirname(__FILE__))
|
21
|
+
|
22
|
+
at_exit do
|
23
|
+
if $!.kind_of?(Exception) && (!$!.kind_of?(SystemExit))
|
24
|
+
$stderr.puts <<"END"
|
25
|
+
This program has encountered an error and cannot continue. Please see the log
|
26
|
+
for details.
|
27
|
+
END
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
Main do
|
33
|
+
description 'A tool for working with Amazon SimpleDB'
|
34
|
+
|
35
|
+
class NullObject
|
36
|
+
def methods_missing(*args, &block)
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize
|
42
|
+
HighLine.track_eof = false
|
43
|
+
RightAws::RightAWSParser.xml_lib = 'libxml'
|
44
|
+
end
|
45
|
+
|
46
|
+
def timestamp
|
47
|
+
Time.now.strftime('%Y%m%d%H%M')
|
48
|
+
end
|
49
|
+
|
50
|
+
option 'access_key' do
|
51
|
+
description 'Amazon AWS access key'
|
52
|
+
argument :required
|
53
|
+
default ENV.fetch('AMAZON_ACCESS_KEY_ID') {
|
54
|
+
ENV.fetch('AWS_ACCESS_KEY_ID')
|
55
|
+
}
|
56
|
+
attr
|
57
|
+
end
|
58
|
+
|
59
|
+
option 'secret_key' do
|
60
|
+
description 'Amazon AWS secret key'
|
61
|
+
argument :required
|
62
|
+
default ENV.fetch('AMAZON_SECRET_ACCESS_KEY') {
|
63
|
+
ENV.fetch('AWS_SECRET_ACCESS_KEY')
|
64
|
+
}
|
65
|
+
attr
|
66
|
+
end
|
67
|
+
|
68
|
+
option 'server' do
|
69
|
+
description 'SimpleDB server hostname'
|
70
|
+
argument :required
|
71
|
+
default 'sdb.amazonaws.com'
|
72
|
+
end
|
73
|
+
|
74
|
+
option 'port' do
|
75
|
+
description 'SimpleDB server port number'
|
76
|
+
argument :required
|
77
|
+
cast :int
|
78
|
+
default 443
|
79
|
+
end
|
80
|
+
|
81
|
+
option 'protocol' do
|
82
|
+
description 'SimpleDB protocol'
|
83
|
+
argument :required
|
84
|
+
default 'https'
|
85
|
+
end
|
86
|
+
|
87
|
+
option 'nil-rep' do
|
88
|
+
description 'How to represent nil values'
|
89
|
+
argument :required
|
90
|
+
default '<[<[<NIL>]>]>'
|
91
|
+
end
|
92
|
+
|
93
|
+
option 'progress' do
|
94
|
+
description "Show progress bar"
|
95
|
+
cast :bool
|
96
|
+
default true
|
97
|
+
attr
|
98
|
+
end
|
99
|
+
|
100
|
+
option 'verbose' do
|
101
|
+
description "Verbose output"
|
102
|
+
cast :bool
|
103
|
+
default false
|
104
|
+
attr
|
105
|
+
end
|
106
|
+
|
107
|
+
option 'chunk-size', 'n' do
|
108
|
+
description "The number of items to process at once"
|
109
|
+
cast :int
|
110
|
+
default 100
|
111
|
+
end
|
112
|
+
|
113
|
+
option 'log' do
|
114
|
+
description 'File to log to'
|
115
|
+
default 'sdbtool.log'
|
116
|
+
attr
|
117
|
+
end
|
118
|
+
|
119
|
+
mixin 'domain' do
|
120
|
+
argument 'domain' do
|
121
|
+
description 'The SimpleDB domain to work on'
|
122
|
+
optional
|
123
|
+
end
|
124
|
+
|
125
|
+
fattr(:domain) {
|
126
|
+
params['domain'].value || choose(*db.domains) do |q|
|
127
|
+
q.prompt = "Domain to operate on?"
|
128
|
+
end
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
fattr(:db) {
|
134
|
+
SimpleDB::Database.new(
|
135
|
+
access_key,
|
136
|
+
secret_key,
|
137
|
+
:server => params['server'].value,
|
138
|
+
:port => params['port'].value,
|
139
|
+
:logger => logger,
|
140
|
+
:nil_representation => params['nil-rep'].value,
|
141
|
+
:protocol => params['protocol'].value)
|
142
|
+
}
|
143
|
+
|
144
|
+
def after_parse_parameters
|
145
|
+
end
|
146
|
+
|
147
|
+
def before_run
|
148
|
+
stderr 'sdbtool-errors.log'
|
149
|
+
logger ::Logger.new(log)
|
150
|
+
unless verbose
|
151
|
+
logger.level = ::Logger::WARN
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
mode 'dump' do
|
156
|
+
description 'Dump the contents of a domain to a file'
|
157
|
+
|
158
|
+
mixin 'domain'
|
159
|
+
|
160
|
+
argument 'file' do
|
161
|
+
description 'File to dump into'
|
162
|
+
optional
|
163
|
+
attr
|
164
|
+
end
|
165
|
+
|
166
|
+
def run
|
167
|
+
self.file ||= ask("Filename for this dump?") do |q|
|
168
|
+
q.default = "#{domain}-#{timestamp}.yaml"
|
169
|
+
end
|
170
|
+
dump_domain = db.domain(domain)
|
171
|
+
logger.info "Preparing to dump domain #{dump_domain.name}"
|
172
|
+
dump = db.make_dump(dump_domain, file)
|
173
|
+
dump.chunk_size = params['chunk-size'].value
|
174
|
+
count = dump.incomplete_count
|
175
|
+
logger.info "Dumping #{count} objects to #{file}"
|
176
|
+
pbar = progress ? ProgressBar.new("dump", count,stdout) : NullObject.new
|
177
|
+
dump.callback = lambda do |item_name|
|
178
|
+
logger.info "Dumped #{item_name}"
|
179
|
+
pbar.inc
|
180
|
+
end
|
181
|
+
dump.start!
|
182
|
+
pbar.finish
|
183
|
+
logger.info "Dump succeeded"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
mode 'count' do
|
188
|
+
description 'Count the number of items in a domain'
|
189
|
+
mixin 'domain'
|
190
|
+
|
191
|
+
def run
|
192
|
+
domain = db.domain(params['domain'].value)
|
193
|
+
puts domain.count
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
mode 'load' do
|
198
|
+
description 'Load a domain from a file'
|
199
|
+
|
200
|
+
mixin 'domain'
|
201
|
+
|
202
|
+
argument 'file' do
|
203
|
+
description 'The file to load from'
|
204
|
+
attr
|
205
|
+
end
|
206
|
+
|
207
|
+
def run
|
208
|
+
target_domain = if db.domain_exists?(domain)
|
209
|
+
db.domain(domain)
|
210
|
+
else
|
211
|
+
logger.info "Creating domain #{domain}"
|
212
|
+
db.create_domain(domain)
|
213
|
+
end
|
214
|
+
say "About to load domain #{target_domain.name} from #{file}"
|
215
|
+
return unless agree("Are you sure you want to continue?")
|
216
|
+
load = db.make_load(target_domain, file)
|
217
|
+
load.chunk_size = params['chunk-size'].value
|
218
|
+
count = load.incomplete_count
|
219
|
+
pbar = progress ? ProgressBar.new("load", count, stdout) : NullObject.new
|
220
|
+
load.callback = lambda do |item_name|
|
221
|
+
logger.info "Loaded #{item_name}"
|
222
|
+
pbar.inc
|
223
|
+
end
|
224
|
+
load.start!
|
225
|
+
pbar.finish
|
226
|
+
logger.info "Finished loading #{file} into #{domain}"
|
227
|
+
end
|
228
|
+
|
229
|
+
end
|
230
|
+
|
231
|
+
mode 'status' do
|
232
|
+
description 'Show the status of an operation'
|
233
|
+
|
234
|
+
argument 'title' do
|
235
|
+
description 'The title of the operation'
|
236
|
+
optional
|
237
|
+
attr
|
238
|
+
end
|
239
|
+
|
240
|
+
option 'failed' do
|
241
|
+
description 'Show details about failed items'
|
242
|
+
default false
|
243
|
+
cast :bool
|
244
|
+
end
|
245
|
+
|
246
|
+
def run
|
247
|
+
self.title ||= choose(*Dir['*.simpledb_op_status'])
|
248
|
+
status_file = if title =~ /\.simpledb_op_status$/
|
249
|
+
title
|
250
|
+
else
|
251
|
+
title + '.simpledb_op_status'
|
252
|
+
end
|
253
|
+
task = SimpleDB::Task.new(status_file, logger)
|
254
|
+
if params['failed'].given?
|
255
|
+
task.failed_items.each do |item_name, info|
|
256
|
+
puts item_name + ": "
|
257
|
+
puts
|
258
|
+
puts info
|
259
|
+
puts "-" * 60
|
260
|
+
end
|
261
|
+
else
|
262
|
+
puts task.report
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
mode 'info' do
|
268
|
+
description 'Show information about a dump file'
|
269
|
+
|
270
|
+
argument 'file' do
|
271
|
+
description 'The dump file to examine'
|
272
|
+
attr
|
273
|
+
end
|
274
|
+
|
275
|
+
def run
|
276
|
+
dump_file = SimpleDB::DumpFile.new(file)
|
277
|
+
puts "File #{file} contains #{dump_file.size} records"
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
mode 'delete-domain' do
|
282
|
+
description "Delete a SimpleDB domain"
|
283
|
+
mixin 'domain'
|
284
|
+
|
285
|
+
def run
|
286
|
+
2.times do
|
287
|
+
return unless agree("Are you sure you want to delete #{params['domain'].value}?") # "
|
288
|
+
end
|
289
|
+
db.delete_domain(params['domain'].value)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
mode 'create-domain' do
|
294
|
+
description "Create a SimpleDB domain"
|
295
|
+
|
296
|
+
mixin 'domain'
|
297
|
+
|
298
|
+
def run
|
299
|
+
db.create_domain(params['domain'].value)
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
mode 'show' do
|
304
|
+
description "Show a single SimpleDB item"
|
305
|
+
mixin 'domain'
|
306
|
+
|
307
|
+
argument 'item_name' do
|
308
|
+
description "The name of the item to show"
|
309
|
+
arity -1 # inifinite
|
310
|
+
end
|
311
|
+
|
312
|
+
def run
|
313
|
+
db.domain(domain).items(params['item_name'].values).each do |item|
|
314
|
+
puts item.to_yaml
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
mode 'list' do
|
320
|
+
mixin 'domain'
|
321
|
+
description "List all objects matching SELECT query"
|
322
|
+
|
323
|
+
argument 'query' do
|
324
|
+
description "A SimpleDB SELECT where-clause"
|
325
|
+
end
|
326
|
+
|
327
|
+
def run
|
328
|
+
logger.info "Selecting items where '#{params['query'].value}'"
|
329
|
+
items = db.domain(domain).select(params['query'].value)
|
330
|
+
logger.info "Selected #{items.count} items"
|
331
|
+
items.each do |item|
|
332
|
+
puts item['itemName()']
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
mode 'remove' do
|
338
|
+
mixin 'domain'
|
339
|
+
description "Delete an item by name"
|
340
|
+
|
341
|
+
argument 'item_name' do
|
342
|
+
description "The name of the item to delete"
|
343
|
+
end
|
344
|
+
|
345
|
+
def run
|
346
|
+
db.domain(domain).delete(params['item_name'].value)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def run
|
351
|
+
help!
|
352
|
+
end
|
353
|
+
end
|
data/lib/sdbtools.rb
ADDED
@@ -0,0 +1,348 @@
|
|
1
|
+
require 'fattr'
|
2
|
+
require 'right_aws'
|
3
|
+
|
4
|
+
module SimpleDB
|
5
|
+
class Operation
|
6
|
+
include Enumerable
|
7
|
+
|
8
|
+
def initialize(sdb, method, *args)
|
9
|
+
@sdb = sdb
|
10
|
+
@method = method
|
11
|
+
@args = args
|
12
|
+
end
|
13
|
+
|
14
|
+
def each
|
15
|
+
next_token = nil
|
16
|
+
begin
|
17
|
+
args = @args.dup
|
18
|
+
args << next_token
|
19
|
+
results = @sdb.send(@method, *args)
|
20
|
+
yield(results)
|
21
|
+
next_token = results[:next_token]
|
22
|
+
end while next_token
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class Database
|
28
|
+
def initialize(access_key=nil, secret_key=nil, options={})
|
29
|
+
@sdb = RightAws::SdbInterface.new(access_key, secret_key, options)
|
30
|
+
@logger = options.fetch(:logger){::Logger.new($stderr)}
|
31
|
+
end
|
32
|
+
|
33
|
+
def domains
|
34
|
+
domains_op = Operation.new(@sdb, :list_domains, nil)
|
35
|
+
domains_op.inject([]) {|domains, results|
|
36
|
+
domains.concat(results[:domains])
|
37
|
+
domains
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def domain(domain_name)
|
42
|
+
Domain.new(@sdb, domain_name)
|
43
|
+
end
|
44
|
+
|
45
|
+
def domain_exists?(domain_name)
|
46
|
+
domains.include?(domain_name)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_domain(domain_name)
|
50
|
+
@sdb.create_domain(domain_name)
|
51
|
+
domain(domain_name)
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete_domain(domain_name)
|
55
|
+
@sdb.delete_domain(domain_name)
|
56
|
+
end
|
57
|
+
|
58
|
+
def make_dump(domain, filename)
|
59
|
+
Dump.new(domain, filename, @logger)
|
60
|
+
end
|
61
|
+
|
62
|
+
def make_load(domain, filename)
|
63
|
+
Load.new(domain, filename, @logger)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
end
|
68
|
+
|
69
|
+
class Domain
|
70
|
+
attr_reader :name
|
71
|
+
|
72
|
+
def initialize(sdb, name)
|
73
|
+
@sdb = sdb
|
74
|
+
@name = name
|
75
|
+
@item_names = nil
|
76
|
+
@count = nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def [](item_name)
|
80
|
+
@sdb.get_attributes(name, item_name)[:attributes]
|
81
|
+
end
|
82
|
+
|
83
|
+
def item_names
|
84
|
+
return @item_names if @item_names
|
85
|
+
query = Operation.new(@sdb, :query, @name, nil, nil)
|
86
|
+
@item_names = query.inject([]) {|names, results|
|
87
|
+
names.concat(results[:items])
|
88
|
+
names
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def count
|
93
|
+
return @count if @count
|
94
|
+
op = Operation.new(@sdb, :select, "select count(*) from #{name}")
|
95
|
+
@count = op.inject(0) {|count, results|
|
96
|
+
count += results[:items].first["Domain"]["Count"].first.to_i
|
97
|
+
count
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def items(item_names)
|
102
|
+
names = item_names.map{|n| "'#{n}'"}.join(', ')
|
103
|
+
query = "select * from #{name} where itemName() in (#{names})"
|
104
|
+
select = Operation.new(@sdb, :select, query)
|
105
|
+
select.inject({}) {|items, results|
|
106
|
+
results[:items].each do |item|
|
107
|
+
item_name = item.keys.first
|
108
|
+
item_value = item.values.first
|
109
|
+
items[item_name] = item_value
|
110
|
+
end
|
111
|
+
items
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
def put(item_name, attributes)
|
116
|
+
@sdb.put_attributes(@name, item_name, attributes)
|
117
|
+
end
|
118
|
+
|
119
|
+
def select(query)
|
120
|
+
op = Operation.new(@sdb, :select, "select * from #{name} where #{query}")
|
121
|
+
op.inject([]){|items,results|
|
122
|
+
batch_items = results[:items].map{|pair|
|
123
|
+
item = pair.values.first
|
124
|
+
item.merge!({'itemName()' => pair.keys.first})
|
125
|
+
item
|
126
|
+
}
|
127
|
+
items.concat(batch_items)
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def delete(item_name)
|
132
|
+
@sdb.delete_attributes(@name, item_name)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Task
|
137
|
+
fattr :callback => lambda{}
|
138
|
+
fattr :chunk_size => 100
|
139
|
+
|
140
|
+
def initialize(status_file, logger)
|
141
|
+
@logger = logger
|
142
|
+
@status_file = Pathname(status_file)
|
143
|
+
@status = PStore.new(@status_file.to_s)
|
144
|
+
unless @status_file.exist?
|
145
|
+
initialize_status!(yield)
|
146
|
+
end
|
147
|
+
@status.transaction(false) do
|
148
|
+
@logger.info "Initializing #{@status_file} for PID #{$$}"
|
149
|
+
@status[$$] = {}
|
150
|
+
@status[$$][:working_items] = []
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def session
|
155
|
+
yield
|
156
|
+
ensure
|
157
|
+
release_working_items!
|
158
|
+
end
|
159
|
+
|
160
|
+
def incomplete_count
|
161
|
+
@status.transaction(true) {|status|
|
162
|
+
Array(status[:incomplete_items]).size
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
def failed_items
|
167
|
+
@status.transaction(true) do |status|
|
168
|
+
status[:failed_items]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def reserve_items!(size)
|
173
|
+
@status.transaction(false) do |status|
|
174
|
+
chunk = status[:incomplete_items].slice!(0,size)
|
175
|
+
status[$$][:working_items].concat(chunk)
|
176
|
+
@logger.info("Reserved #{chunk.size} items")
|
177
|
+
chunk
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def finish_items!(items)
|
182
|
+
@status.transaction(false) do |status|
|
183
|
+
items.each do |item_name|
|
184
|
+
status[:complete_items] <<
|
185
|
+
status[$$][:working_items].delete(item_name)
|
186
|
+
@logger.info("Marked item #{item_name} complete")
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def release_working_items!
|
192
|
+
@logger.info("Releasing working items")
|
193
|
+
@status.transaction(false) do |status|
|
194
|
+
items = status[$$][:working_items]
|
195
|
+
status[:incomplete_items].concat(items)
|
196
|
+
status[$$][:working_items].clear
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def report
|
201
|
+
@status.transaction(true) do |status|
|
202
|
+
done = status[:complete_items].size
|
203
|
+
not_done = status[:incomplete_items].size
|
204
|
+
failed = status[:failed_items].size
|
205
|
+
puts "Items (not done/done/failed): #{not_done}/#{done}/#{failed}"
|
206
|
+
status.roots.select{|root| root.kind_of?(Integer)}.each do |root|
|
207
|
+
pid = root
|
208
|
+
items = status[root][:working_items].size
|
209
|
+
puts "Process #{pid} working on #{items} items"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def record_failed_item!(item_name, info)
|
217
|
+
@logger.info "Problem with item #{item_name}"
|
218
|
+
@status.transaction(false) do |status|
|
219
|
+
status[:failed_items] ||= {}
|
220
|
+
status[:failed_items][item_name] = info
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def initialize_status!(item_names)
|
225
|
+
@status.transaction(false) do |status|
|
226
|
+
status[:incomplete_items] = item_names
|
227
|
+
status[:failed_items] = {}
|
228
|
+
status[:complete_items] = []
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class Dump < Task
|
234
|
+
def initialize(domain, filename, logger)
|
235
|
+
@domain = domain
|
236
|
+
@dump_filename = Pathname(filename)
|
237
|
+
super(status_filename, logger) {
|
238
|
+
@domain.item_names
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
def status_filename
|
243
|
+
Pathname(
|
244
|
+
@dump_filename.basename(@dump_filename.extname).to_s +
|
245
|
+
".simpledb_op_status")
|
246
|
+
end
|
247
|
+
|
248
|
+
def start!
|
249
|
+
session do
|
250
|
+
until (chunk = reserve_items!(chunk_size)).empty?
|
251
|
+
items = @domain.items(chunk)
|
252
|
+
dump_items(items)
|
253
|
+
finish_items!(chunk)
|
254
|
+
items.each do |item| callback.call(item) end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
private
|
260
|
+
|
261
|
+
def dump_items(items)
|
262
|
+
@logger.info "Dumping #{items.size} items to #{@dump_filename}"
|
263
|
+
FileUtils.touch(@dump_filename) unless @dump_filename.exist?
|
264
|
+
file = File.new(@dump_filename.to_s)
|
265
|
+
file.flock(File::LOCK_EX)
|
266
|
+
@dump_filename.open('a+') do |f|
|
267
|
+
items.each_pair do |item_name, attributes|
|
268
|
+
@logger.info "Dumping item #{item_name}"
|
269
|
+
YAML.dump({item_name => attributes}, f)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
ensure
|
273
|
+
file.flock(File::LOCK_UN)
|
274
|
+
end
|
275
|
+
|
276
|
+
end
|
277
|
+
|
278
|
+
class Load < Task
|
279
|
+
def initialize(domain, filename, logger)
|
280
|
+
@domain = domain
|
281
|
+
@dump_filename = Pathname(filename)
|
282
|
+
@dump_file = DumpFile.new(filename)
|
283
|
+
super(status_filename, logger) {
|
284
|
+
@dump_file.item_names
|
285
|
+
}
|
286
|
+
end
|
287
|
+
|
288
|
+
def status_filename
|
289
|
+
Pathname(
|
290
|
+
@dump_filename.basename(@dump_filename.extname).to_s +
|
291
|
+
"-load-#{@domain.name}.simpledb_op_status")
|
292
|
+
end
|
293
|
+
|
294
|
+
def start!
|
295
|
+
session do
|
296
|
+
chunk = []
|
297
|
+
reserved = Set.new
|
298
|
+
@dump_file.each do |item_name, attributes|
|
299
|
+
if reserved.empty?
|
300
|
+
finish_items!(chunk)
|
301
|
+
reserved.replace(chunk = reserve_items!(chunk_size))
|
302
|
+
break if chunk.empty?
|
303
|
+
end
|
304
|
+
if reserved.include?(item_name)
|
305
|
+
@logger.info("#{item_name} is reserved, loading to #{@domain.name}")
|
306
|
+
begin
|
307
|
+
@domain.put(item_name, attributes)
|
308
|
+
rescue
|
309
|
+
record_failed_item!(
|
310
|
+
item_name,
|
311
|
+
"#{$!.class.name}: #{$!.message}")
|
312
|
+
end
|
313
|
+
reserved.delete(item_name)
|
314
|
+
callback.call(item_name)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
finish_items!(chunk)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
class DumpFile
|
323
|
+
include Enumerable
|
324
|
+
|
325
|
+
def initialize(path)
|
326
|
+
@path = Pathname(path)
|
327
|
+
end
|
328
|
+
|
329
|
+
def item_names
|
330
|
+
map{|item_name, item| item_name}
|
331
|
+
end
|
332
|
+
|
333
|
+
def size
|
334
|
+
inject(0){|size, item_name, item|
|
335
|
+
size += 1
|
336
|
+
}
|
337
|
+
end
|
338
|
+
|
339
|
+
def each
|
340
|
+
@path.open('r') {|f|
|
341
|
+
YAML.load_documents(f) {|doc|
|
342
|
+
yield doc.keys.first, doc.values.first
|
343
|
+
}
|
344
|
+
}
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: sdbtools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Avdi Grimm
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-01-18 00:00:00 -05:00
|
13
|
+
default_executable: sdbtool
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: right_aws
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "1.10"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.9
|
34
|
+
version:
|
35
|
+
description: |
|
36
|
+
SDBTools layers a higher-level OO interface on top of RightAWS, as well as
|
37
|
+
providing some command-line utilities for working with SimpleDB.
|
38
|
+
|
39
|
+
email: devs@devver.net
|
40
|
+
executables:
|
41
|
+
- sdbtool
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
files:
|
47
|
+
- Rakefile
|
48
|
+
- VERSION
|
49
|
+
- bin/sdbtool
|
50
|
+
- lib/sdbtools.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/devver/sdbtools
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options:
|
57
|
+
- --charset=UTF-8
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: "0"
|
65
|
+
version:
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: "0"
|
71
|
+
version:
|
72
|
+
requirements: []
|
73
|
+
|
74
|
+
rubyforge_project:
|
75
|
+
rubygems_version: 1.3.5
|
76
|
+
signing_key:
|
77
|
+
specification_version: 3
|
78
|
+
summary: A high-level OO interface to Amazon SimpleDB
|
79
|
+
test_files: []
|
80
|
+
|