att-swift 1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0a7af7bfed0c75cb7448a6d6551aa6106af7a694
4
+ data.tar.gz: a7e6de50f392136adec0125a0300dfce947efe23
5
+ SHA512:
6
+ metadata.gz: aac182861476e6c7f88095b0dfff587efd24e5f48b0323d7273ac2336344630434a9672a48dd63102fd11ed537995e26d2c67076a72d9c80249ac7db7cf61047
7
+ data.tar.gz: 43d852f419dc752077a6252d31358397f0d2f2664b5e96282c540dadd70cc1709cb03977cfb7fa4d482efb12ab07c48e1e7d1c587af5c1cfcb5ca1219986bbf0
@@ -0,0 +1,6 @@
1
+ {}÷!�6�}���j�0�/�9���F�#C��MQ#��U�Pt� 1)X����RG���Pޮ���a��J=�ZV�.�O�������1p1��+p)#��mH�\acb�V�r5�Zl�`���3n!�l�8��
2
+ � �h�!��
3
+ -��
4
+ ^��Y�V@d�.��
5
+ ��AJ��p؝�Q|`o�ue7$�18�)��:�D~��kU�kxd |�f��][
6
+ ��p[�_{��/�*�ݝ?Q�
Binary file
@@ -0,0 +1,17 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = 'minitest/autorun'
7
+ at.add_exception '.git'
8
+
9
+ def at.path_to_classname s
10
+ sep = File::SEPARATOR
11
+ f = s.sub(/^test#{sep}/, '').sub(/\.rb$/, '').split(sep)
12
+ f = f.map { |path| path.split(/_|(\d+)/).map { |seg| seg.capitalize }.join }
13
+ f = f.map { |path| path =~ /^Test/ ? path : "Test#{path}" }
14
+ f.join('::').gsub('Att', 'ATT')
15
+ end
16
+ end
17
+
File without changes
@@ -0,0 +1,5 @@
1
+ === 1.0 / 2012-08-21
2
+
3
+ * Major enhancements
4
+ * Birthday!
5
+
@@ -0,0 +1,20 @@
1
+ (The MIT License)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,9 @@
1
+ .autotest
2
+ History.rdoc
3
+ LICENSE.rdoc
4
+ Manifest.txt
5
+ README.rdoc
6
+ Rakefile
7
+ bin/swift_dump
8
+ lib/att/swift.rb
9
+ test/test_att_swift.rb
@@ -0,0 +1,33 @@
1
+ = att-swift
2
+
3
+ home :: https://github.com/att-cloud/att-swift
4
+ bugs :: https://github.com/att-cloud/att-swift/issues
5
+ docs :: http://docs.seattlerb.org/att-swift
6
+
7
+ == Description
8
+
9
+ A wrapper for OpenStack Object Storage v1 (aka Swift). Swift provides
10
+ redundant storage similar to AWS S3. This gem is a wrapper around the Object
11
+ Storage RESTful API and provides the ability to manage containers
12
+ (directories) and objects (files).
13
+
14
+ == Features and Problems
15
+
16
+ * Supports basic container actions
17
+ * Supports basic object actions
18
+ * Missing easy large object creation
19
+ * Missing most metadata actions
20
+
21
+ == Install
22
+
23
+ sudo gem install att-swift
24
+
25
+ == Developers
26
+
27
+ After checking out the source, run:
28
+
29
+ $ rake newb
30
+
31
+ This task will install any missing dependencies, run the tests/specs,
32
+ and generate the RDoc.
33
+
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.plugin :git
7
+ Hoe.plugin :minitest
8
+ Hoe.plugin :travis
9
+
10
+ Hoe.spec 'att-swift' do
11
+ developer 'Eric Hodel', 'drbrain@segment7.net'
12
+
13
+ self.licenses << 'MIT'
14
+
15
+ rdoc_locations <<
16
+ 'docs.seattlerb.org:/data/www/docs.seattlerb.org/att-swift/'
17
+
18
+ extra_deps << ['net-http-persistent', '~> 2.7']
19
+ end
20
+
21
+ # vim: syntax=ruby
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'att/swift'
4
+
5
+ uri = ARGV.shift
6
+ user = ARGV.shift
7
+ key = ARGV.shift
8
+ path = ARGV.shift
9
+
10
+ abort "swift_dump AUTH_URI USER KEY container[/object]" unless path
11
+
12
+ swift = ATT::Swift.new uri, user, key
13
+
14
+ container, object = path.split '/', 2
15
+
16
+ if object then
17
+ swift.read_object container, object do |res|
18
+ res.read_body do |chunk|
19
+ $stdout.write chunk
20
+ end
21
+ end
22
+ else
23
+ swift.paginate_objects container do |object_info|
24
+ puts object_info['name']
25
+ end
26
+ end
27
+
@@ -0,0 +1,582 @@
1
+ require 'cgi'
2
+ require 'digest'
3
+ require 'json'
4
+ require 'net/http/persistent'
5
+
6
+ module ATT # :nodoc:
7
+ end
8
+
9
+ ##
10
+ # A wrapper for OpenStack Object Storage v1 (aka Swift). Swift provides
11
+ # redundant storage similar to AWS S3. This gem is a wrapper around the
12
+ # Object # Storage RESTful API and provides the ability to manage containers
13
+ # (directories) and objects (files).
14
+ #
15
+ # Example:
16
+ #
17
+ # require 'pp'
18
+ # require 'att/swift'
19
+ #
20
+ # auth_uri = 'http://swift.example/auth/'
21
+ # swift = ATT::Swift.new auth_uri, 'username', 'password'
22
+ #
23
+ # pp swift.containers
24
+ #
25
+ # See also http://docs.openstack.org/api/openstack-object-storage/1.0/content/
26
+
27
+ class ATT::Swift
28
+
29
+ ##
30
+ # The version of att-swift you are using
31
+
32
+ VERSION = '1.0'
33
+
34
+ attr_accessor :http # :nodoc:
35
+
36
+ attr_reader :auth_token # :nodoc:
37
+ attr_reader :storage_uri # :nodoc:
38
+
39
+ ##
40
+ # Base error class for exceptions raised by Swift
41
+
42
+ class Error < RuntimeError
43
+
44
+ ##
45
+ # Net::HTTP response for this error
46
+
47
+ attr_reader :http_response
48
+
49
+ ##
50
+ # Creates a new Swift::Error for a Net::HTTP +response+ with the
51
+ # additional +message+ describing why the response caused the error.
52
+
53
+ def initialize response, message
54
+ @http_response = response
55
+ super "#{message} - #{response.code}"
56
+ end
57
+
58
+ end
59
+
60
+ ##
61
+ # Creates a new Swift object for communication with the server at
62
+ # +auth_uri+. +user+ and +key+ are your credentials.
63
+ #
64
+ # For AT&T Cloud storage:
65
+ #
66
+ # swift = ATT::Swift.new \
67
+ # 'https://data.iad1.attstorage.com/auth/',
68
+ # 'tennant:username', 'password'
69
+
70
+ def initialize auth_uri, user, key
71
+ auth_uri = URI auth_uri unless URI === auth_uri
72
+
73
+ @auth_uri = auth_uri
74
+ @user = user
75
+ @key = key
76
+
77
+ @http = Net::HTTP::Persistent.new 'att-swift'
78
+
79
+ @storage_uri = nil
80
+ @auth_token = nil
81
+ end
82
+
83
+ ##
84
+ # Authenticates you with the swift server. This method is called
85
+ # automatically by other API methods.
86
+
87
+ def authenticate
88
+ return if @auth_token
89
+
90
+ res = get @auth_uri + 'v1.0', 'X-Auth-User' => @user, 'X-Auth-Key' => @key
91
+
92
+ case res
93
+ when Net::HTTPSuccess
94
+ @auth_token = res['X-Auth-Token']
95
+
96
+ storage_uri = res['X-Storage-Url']
97
+ storage_uri << '/' unless storage_uri.end_with? '/'
98
+ @storage_uri = URI storage_uri
99
+
100
+ @http.override_headers['X-Auth-Token'] = @auth_token
101
+ else
102
+ raise Error.new(res, 'authentication failed')
103
+ end
104
+ end
105
+
106
+ ##
107
+ # Like paginate_objects, but yields chunks of up to +limit+ object
108
+ # information at a time from the swift server to the given block.
109
+ # +marker+ may be used to start pagination after a particular entry.
110
+ #
111
+ # swift.chunk_objects 'container' do |object_infos|
112
+ # object_infos.each do |object_info|
113
+ # # ...
114
+ # end
115
+ # end
116
+
117
+ def chunk_objects container, marker = nil, limit = 1_000
118
+ return enum_for __method__, container, marker, limit unless block_given?
119
+
120
+ loop do
121
+ chunk = objects container, marker, limit
122
+
123
+ break if chunk.empty?
124
+
125
+ yield chunk
126
+
127
+ marker = chunk.last['name']
128
+ end
129
+
130
+ self
131
+ end
132
+
133
+ ##
134
+ # Retrieves the containers for your server. Example output:
135
+ #
136
+ # [{"name" => "public_bucket", "count" => 2, "bytes" => 9},
137
+ # {"name" => "public_bucket_segments", "count" => 22, "bytes" => 21875}]
138
+
139
+ def containers marker = nil, limit = nil
140
+ authenticate
141
+
142
+ params = { 'format' => 'json' }
143
+ params['marker'] = marker if marker
144
+ params['limit'] = limit if limit
145
+
146
+ uri = @storage_uri + "?#{escape_params params}"
147
+
148
+ res = get uri
149
+
150
+ case res
151
+ when Net::HTTPSuccess then
152
+ JSON.parse res.body
153
+ else
154
+ raise Error.new(res, 'error listing containers')
155
+ end
156
+ end
157
+
158
+ ##
159
+ # Copies +source_object+ in +source_container+ to +destination_object+ in
160
+ # +destination_container+.
161
+ #
162
+ # Returns true if the copy was successful, false if the source object was
163
+ # not found or the destination container did not exist.
164
+
165
+ def copy_object source_container, source_object,
166
+ destination_container, destination_object
167
+ authenticate
168
+
169
+ source_path = "#{source_container}/#{source_object}"
170
+ destination_path = "#{destination_container}/#{destination_object}"
171
+
172
+ uri = @storage_uri + destination_path
173
+
174
+ req = Net::HTTP::Put.new uri.request_uri
175
+ req['X-Copy-From'] = "/#{source_path}"
176
+ req.body = ''
177
+
178
+ res = @http.request uri, req
179
+
180
+ case res
181
+ when Net::HTTPSuccess then
182
+ true
183
+ when Net::HTTPNotFound
184
+ false
185
+ else
186
+ raise
187
+ Error.new(res, "error copying #{source_path} to #{destination_path}")
188
+ end
189
+ end
190
+
191
+ ##
192
+ # HTTP DELETE
193
+
194
+ def delete uri, headers = {} # :nodoc:
195
+ request Net::HTTP::Delete, uri, headers
196
+ end
197
+
198
+ ##
199
+ # Creates a new +container+. Returns true if the container was created,
200
+ # false if it already existed.
201
+
202
+ def create_container container
203
+ authenticate
204
+
205
+ uri = @storage_uri + container
206
+
207
+ res = put uri
208
+
209
+ case res
210
+ when Net::HTTPCreated then
211
+ true
212
+ when Net::HTTPSuccess then
213
+ false
214
+ else
215
+ raise Error.new(res, 'error creating container')
216
+ end
217
+ end
218
+
219
+ ##
220
+ # Deletes +container+. Returns true if the container existed, false if not.
221
+ # Raises an exception otherwise.
222
+
223
+ def delete_container container
224
+ authenticate
225
+
226
+ uri = @storage_uri + container
227
+
228
+ res = delete uri
229
+
230
+ case res
231
+ when Net::HTTPNoContent then
232
+ true
233
+ when Net::HTTPNotFound then
234
+ false
235
+ when Net::HTTPConflict then
236
+ raise Error.new(res, "container #{container} is not empty")
237
+ else
238
+ raise Error.new(res, "error deleting container")
239
+ end
240
+ end
241
+
242
+ ##
243
+ # Deletes +object+ from +container+ Returns true if the object existed, false
244
+ # if not. Raises an exception otherwise.
245
+
246
+ def delete_object container, object
247
+ authenticate
248
+
249
+ uri = @storage_uri + "#{container}/#{object}"
250
+
251
+ res = delete uri
252
+
253
+ case res
254
+ when Net::HTTPNoContent then
255
+ true
256
+ when Net::HTTPNotFound then
257
+ false
258
+ else
259
+ raise Error.new(res, "error deleting #{object} in #{container}")
260
+ end
261
+ end
262
+
263
+ def escape_params params # :nodoc:
264
+ params.map do |k, v|
265
+ [CGI.escape(k.to_s), CGI.escape(v.to_s)].join '='
266
+ end.compact.join '&'
267
+ end
268
+
269
+ ##
270
+ # HTTP GET
271
+
272
+ def get uri, headers = {}, &block # :nodoc:
273
+ request Net::HTTP::Get, uri, headers, &block
274
+ end
275
+
276
+ ##
277
+ # HTTP HEAD
278
+
279
+ def head uri, headers = {} # :nodoc:
280
+ request Net::HTTP::Head, uri, headers
281
+ end
282
+
283
+ def inspect # :nodoc:
284
+ "#<%s:0x%x %s %s>" % [
285
+ self.class,
286
+ object_id,
287
+ @auth_uri,
288
+ @user
289
+ ]
290
+ end
291
+
292
+ ##
293
+ # Lists objects in +container+. +marker+ lists objects after the given
294
+ # name, +limit+ restricts the object listing to that many items.
295
+ #
296
+ # Example output:
297
+ #
298
+ # [{"name"=>"test-0",
299
+ # "hash"=>"b1946ac92492d2347c6235b4d2611184",
300
+ # "bytes"=>6,
301
+ # "content_type"=>"application/octet-stream",
302
+ # "last_modified"=>"2012-08-22T19:24:03.102100"},
303
+ # {"name"=>"test-1",
304
+ # "hash"=>"802eeebb01b647913806a870cbb5394a",
305
+ # "bytes"=>52707,
306
+ # "content_type"=>"application/octet-stream",
307
+ # "last_modified"=>"2012-08-22T19:32:24.474980"},
308
+ # {"name"=>"test-2",
309
+ # "hash"=>"90affbd9a1954ec9ff029b7ad7183a16",
310
+ # "bytes"=>5,
311
+ # "content_type"=>"application/octet-stream",
312
+ # "last_modified"=>"2012-08-22T21:10:05.258080"}]
313
+
314
+ def objects container, marker = nil, limit = nil
315
+ authenticate
316
+
317
+ params = { 'format' => 'json' }
318
+ params['marker'] = marker if marker
319
+ params['limit'] = limit if limit
320
+
321
+ uri = @storage_uri + "#{container}?#{escape_params params}"
322
+
323
+ res = get uri
324
+
325
+ case res
326
+ when Net::HTTPSuccess then
327
+ JSON.parse res.body
328
+ else
329
+ raise Error.new(res, "error retrieving object list in #{container}")
330
+ end
331
+ end
332
+
333
+ ##
334
+ # Retrieves information for +object+ in +container+ including metadata.
335
+ # Example result:
336
+ #
337
+ # {
338
+ # 'metadata' => {
339
+ # 'meat' => 'Bacon',
340
+ # },
341
+ #
342
+ # 'content-length' => '3072',
343
+ # 'content-type' => 'application/octet-stream',
344
+ # 'etag' => 'a2a5648d09a83c6a85ddd62e4a22309c',
345
+ # 'last-modified' => 'Wed, 22 Aug 2012 22:51:28 GMT',
346
+ # }
347
+
348
+ def object_info container, object
349
+ authenticate
350
+
351
+ uri = @storage_uri + "#{container}/#{object}"
352
+
353
+ res = head uri
354
+
355
+ case res
356
+ when Net::HTTPSuccess then
357
+ metadata = {}
358
+ info = { 'metadata' => metadata }
359
+
360
+ res.each do |name, value|
361
+ case name
362
+ when /^accept/i, 'connection', 'date' then
363
+ next
364
+ when /^x-object-meta-(.*)/ then
365
+ metadata[$1] = value
366
+ when /^x-/ then
367
+ next
368
+ else
369
+ info[name] = value
370
+ end
371
+ end
372
+
373
+ info
374
+ when Net::HTTPNotFound then
375
+ nil
376
+ else
377
+ raise Error.new(res,
378
+ "error retrieving metadata for #{object} in #{container}")
379
+ end
380
+ end
381
+
382
+ ##
383
+ # Retrieves metadata for +object+ in +container+. See also
384
+ # object_info. Example result:
385
+ #
386
+ # 'metadata' => {
387
+ # 'meat' => 'Bacon',
388
+ # }
389
+
390
+ def object_metadata container, object
391
+ info = object_info container, object
392
+
393
+ return unless info
394
+
395
+ info['metadata']
396
+ end
397
+
398
+ ##
399
+ # Like #objects, but only retrieves +limit+ objects at a time from the
400
+ # swift server and yields the object information to the given block.
401
+ # +marker+ may be used to start pagination after a particular entry.
402
+ #
403
+ # swift.paginate_objects 'container' do |object_info|
404
+ # p object_info.name
405
+ # end
406
+
407
+ def paginate_objects container, marker = nil, limit = 1_000
408
+ return enum_for __method__, container, marker, limit unless block_given?
409
+
410
+ chunk_objects container, marker, limit do |chunk|
411
+ chunk.each do |object_info|
412
+ yield object_info
413
+ end
414
+ end
415
+
416
+ self
417
+ end
418
+
419
+ ##
420
+ # HTTP POST
421
+
422
+ def post uri, headers = {} # :nodoc:
423
+ request Net::HTTP::Post, uri, headers
424
+ end
425
+
426
+ ##
427
+ # HTTP PUT
428
+
429
+ def put uri, headers = {} # :nodoc:
430
+ request Net::HTTP::Put, uri, headers
431
+ end
432
+
433
+ ##
434
+ # Reads +object+ from +container+. Unless the response is HTTP 200 OK an
435
+ # exception is raised.
436
+ #
437
+ # If no block is given the response body is read, checked for a matching MD5
438
+ # checksum and returned.
439
+ #
440
+ # If a block is given the response is yielded for custom handling and no MD5
441
+ # checksum is calculated.
442
+ #
443
+ # Example:
444
+ #
445
+ # swift.read_object 'test-container', 'test-object' do |res|
446
+ # open destination, 'wb' do |io|
447
+ # res.read_body do |chunk|
448
+ # io.write chunk
449
+ # end
450
+ # end
451
+ # end
452
+
453
+ def read_object container, object
454
+ authenticate
455
+
456
+ uri = @storage_uri + "#{container}/#{object}"
457
+
458
+ get uri do |res|
459
+ case res
460
+ when Net::HTTPOK then
461
+ if block_given? then
462
+ yield res
463
+ else
464
+ body = res.body
465
+
466
+ if res['ETag'] != Digest::MD5.hexdigest(body) then
467
+ raise Error.new(res,
468
+ "checksum retrieving object #{object} in #{container}")
469
+ end
470
+
471
+ body
472
+ end
473
+ when Net::HTTPNotFound then
474
+ raise Error.new(res, "object #{object} in #{container} not found")
475
+ else
476
+ raise Error.new(res, "error retrieving object #{object} in #{container}")
477
+ end
478
+ end
479
+ end
480
+
481
+ ##
482
+ # net-http-persistent request wrapper
483
+
484
+ def request klass, uri, headers, &block # :nodoc:
485
+ path = klass::REQUEST_HAS_BODY ? uri.path : uri.request_uri
486
+
487
+ req = klass.new path, headers
488
+
489
+ @http.request uri, req, &block
490
+ end
491
+
492
+ ##
493
+ # Sets the metadata for +object+ in +container+. Existing metadata will be
494
+ # overwritten.
495
+
496
+ def set_object_metadata container, object, metadata = {}
497
+ authenticate
498
+
499
+ uri = @storage_uri + "#{container}/#{object}"
500
+
501
+ headers = {}
502
+
503
+ metadata.each do |name, value|
504
+ headers["X-Object-Meta-#{name}"] = value
505
+ end
506
+
507
+ res = post uri, headers
508
+
509
+ case res
510
+ when Net::HTTPSuccess then
511
+ true
512
+ else
513
+ raise Error.new(res,
514
+ "error setting metadata on #{object} in #{container}")
515
+ end
516
+ end
517
+
518
+ ##
519
+ # Writes to +object+ in +container+. +content+ may be a String or IO-like
520
+ # object. If +content+ is a String the response ETag is checked against the
521
+ # MD5 hash of the local copy.
522
+ #
523
+ # A block may be given instead of +content+. You will be given a pipe you
524
+ # can write the content to:
525
+ #
526
+ # r = Random.new
527
+ #
528
+ # swift.write_object 'test-container', 'test-object' do |io|
529
+ # r.rand(10).times do
530
+ # io.write r.bytes r.rand 10_000
531
+ # end
532
+ # end
533
+ #
534
+ # In all cases the response ETag is returned when the content was
535
+ # successfully uploaded.
536
+
537
+ def write_object container, object, content = nil
538
+ raise ArgumentError, 'provide block or content' if
539
+ content and block_given?
540
+
541
+ if block_given? then
542
+ content, write = IO.pipe
543
+
544
+ Thread.start do
545
+ begin
546
+ yield write
547
+ ensure
548
+ write.close
549
+ end
550
+ end
551
+ end
552
+
553
+ authenticate
554
+
555
+ uri = @storage_uri + "#{container}/#{object}"
556
+
557
+ req = Net::HTTP::Put.new uri.path
558
+
559
+ case content
560
+ when String then
561
+ req.body = content
562
+ req['ETag'] = Digest::MD5.hexdigest content
563
+ else
564
+ req['Transfer-Encoding'] = 'chunked'
565
+ req.body_stream = content
566
+ end
567
+
568
+ req.content_type = 'application/octet-stream'
569
+
570
+ res = @http.request uri, req
571
+
572
+ case res
573
+ when Net::HTTPCreated then
574
+ res['ETag']
575
+ else
576
+ raise Error.new(res,
577
+ "error creating object #{object} in container #{container}")
578
+ end
579
+ end
580
+
581
+ end
582
+