att-swift 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.
@@ -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
+