harbor 0.16.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 +76 -0
- data/bin/harbor +7 -0
- data/lib/harbor.rb +17 -0
- data/lib/harbor/accessor_injector.rb +30 -0
- data/lib/harbor/application.rb +172 -0
- data/lib/harbor/auth/basic.rb +51 -0
- data/lib/harbor/block_io.rb +63 -0
- data/lib/harbor/cache.rb +90 -0
- data/lib/harbor/cache/disk.rb +99 -0
- data/lib/harbor/cache/item.rb +48 -0
- data/lib/harbor/cache/memory.rb +35 -0
- data/lib/harbor/cascade.rb +75 -0
- data/lib/harbor/console.rb +34 -0
- data/lib/harbor/container.rb +134 -0
- data/lib/harbor/contrib/debug.rb +236 -0
- data/lib/harbor/contrib/session/data_mapper.rb +74 -0
- data/lib/harbor/daemon.rb +105 -0
- data/lib/harbor/errors.rb +49 -0
- data/lib/harbor/events.rb +45 -0
- data/lib/harbor/exception_notifier.rb +59 -0
- data/lib/harbor/file.rb +66 -0
- data/lib/harbor/file_store.rb +69 -0
- data/lib/harbor/file_store/file.rb +100 -0
- data/lib/harbor/file_store/local.rb +71 -0
- data/lib/harbor/file_store/mosso.rb +154 -0
- data/lib/harbor/file_store/mosso/private.rb +8 -0
- data/lib/harbor/generator.rb +56 -0
- data/lib/harbor/generator/help.rb +34 -0
- data/lib/harbor/generator/setup.rb +82 -0
- data/lib/harbor/generator/skeletons/basic/config.ru.skel +21 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname.rb.skel +49 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/controllers/home.rb +9 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/home/index.html.erb.skel +23 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/application.html.erb.skel +48 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/exception.html.erb.skel +13 -0
- data/lib/harbor/generator/skeletons/basic/lib/appname/views/layouts/login.html.erb.skel +11 -0
- data/lib/harbor/generator/skeletons/basic/log/development.log +0 -0
- data/lib/harbor/hooks.rb +105 -0
- data/lib/harbor/json_cookies.rb +37 -0
- data/lib/harbor/layouts.rb +61 -0
- data/lib/harbor/locale.rb +50 -0
- data/lib/harbor/locales.txt +22 -0
- data/lib/harbor/logging.rb +39 -0
- data/lib/harbor/logging/appenders/email.rb +84 -0
- data/lib/harbor/logging/request_logger.rb +34 -0
- data/lib/harbor/mail_servers/abstract.rb +12 -0
- data/lib/harbor/mail_servers/sendmail.rb +19 -0
- data/lib/harbor/mail_servers/smtp.rb +25 -0
- data/lib/harbor/mail_servers/test.rb +17 -0
- data/lib/harbor/mailer.rb +96 -0
- data/lib/harbor/messages.rb +17 -0
- data/lib/harbor/mime.rb +206 -0
- data/lib/harbor/plugin.rb +52 -0
- data/lib/harbor/plugin_list.rb +38 -0
- data/lib/harbor/request.rb +138 -0
- data/lib/harbor/response.rb +281 -0
- data/lib/harbor/router.rb +112 -0
- data/lib/harbor/script.rb +155 -0
- data/lib/harbor/session.rb +75 -0
- data/lib/harbor/session/abstract.rb +27 -0
- data/lib/harbor/session/cookie.rb +17 -0
- data/lib/harbor/shellwords.rb +26 -0
- data/lib/harbor/test/mailer.rb +10 -0
- data/lib/harbor/test/request.rb +28 -0
- data/lib/harbor/test/response.rb +17 -0
- data/lib/harbor/test/session.rb +11 -0
- data/lib/harbor/test/test.rb +22 -0
- data/lib/harbor/version.rb +3 -0
- data/lib/harbor/view.rb +89 -0
- data/lib/harbor/view_context.rb +134 -0
- data/lib/harbor/view_context/helpers.rb +7 -0
- data/lib/harbor/view_context/helpers/cache.rb +77 -0
- data/lib/harbor/view_context/helpers/form.rb +34 -0
- data/lib/harbor/view_context/helpers/html.rb +26 -0
- data/lib/harbor/view_context/helpers/text.rb +120 -0
- data/lib/harbor/view_context/helpers/url.rb +11 -0
- data/lib/harbor/xml_view.rb +57 -0
- data/lib/harbor/zipped_io.rb +203 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles.rb +77 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/authentication.rb +46 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/connection.rb +280 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/container.rb +260 -0
- data/lib/vendor/cloudfiles-1.3.0/cloudfiles/storage_object.rb +253 -0
- metadata +155 -0
@@ -0,0 +1,11 @@
|
|
1
|
+
##
|
2
|
+
# Generic URL helpers, such as merging query strings.
|
3
|
+
##
|
4
|
+
module Harbor::ViewContext::Helpers::Url
|
5
|
+
##
|
6
|
+
# Takes a query string and merges the provided params returning a new query string.
|
7
|
+
##
|
8
|
+
def merge_query_string(query_string, params = {})
|
9
|
+
Rack::Utils::build_query(Rack::Utils::parse_query(query_string).merge(params))
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
gem "builder"
|
2
|
+
require "builder"
|
3
|
+
|
4
|
+
module Harbor
|
5
|
+
class XMLViewContext < ViewContext
|
6
|
+
|
7
|
+
def render(partial, variables=nil)
|
8
|
+
context = to_hash
|
9
|
+
|
10
|
+
result = XMLView.new(partial, merge(variables)).to_s
|
11
|
+
|
12
|
+
replace(context)
|
13
|
+
|
14
|
+
result
|
15
|
+
end
|
16
|
+
|
17
|
+
def xml
|
18
|
+
@view.xml
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
class XMLView < View
|
24
|
+
|
25
|
+
attr_accessor :xml, :output
|
26
|
+
|
27
|
+
def initialize(view, context = {})
|
28
|
+
super
|
29
|
+
@content_type = "text/xml"
|
30
|
+
@extension = ".rxml"
|
31
|
+
@output = ""
|
32
|
+
@filename = ::File.extname(view) == "" ? (view + @extension) : view
|
33
|
+
|
34
|
+
if context.is_a?(ViewContext)
|
35
|
+
@context = context
|
36
|
+
@xml = context.view.xml
|
37
|
+
else
|
38
|
+
@xml = Builder::XmlMarkup.new(:indent => 2, :target => @output)
|
39
|
+
@context = XMLViewContext.new(self, context)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def supports_layouts?
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s(layout = nil)
|
48
|
+
path = View::path.detect { |dir| ::File.exists?(dir + @filename) }
|
49
|
+
raise "Could not find '#{@filename}' in #{View::path.inspect}" if path.nil?
|
50
|
+
|
51
|
+
eval_code = ::File.read(path + @filename)
|
52
|
+
XMLViewContext.new(self, @context).instance_eval(eval_code, __FILE__, __LINE__)
|
53
|
+
|
54
|
+
@output
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
require "zlib"
|
2
|
+
|
3
|
+
module Harbor
|
4
|
+
|
5
|
+
# An IO class for zipping files suitable for sending via rack.
|
6
|
+
class ZippedIO
|
7
|
+
|
8
|
+
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
|
9
|
+
END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
|
10
|
+
|
11
|
+
def initialize(files)
|
12
|
+
@files = files
|
13
|
+
end
|
14
|
+
|
15
|
+
def each
|
16
|
+
zip_entries.each do |entry|
|
17
|
+
yield entry.read_local_entry
|
18
|
+
|
19
|
+
deflater = Deflater.new(entry.file)
|
20
|
+
deflater.read do |data|
|
21
|
+
yield data
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
zip_central_directory.read do |data|
|
26
|
+
yield data
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def size
|
31
|
+
return @size if @size
|
32
|
+
@size = 0
|
33
|
+
zip_entries.each do |entry|
|
34
|
+
@size += entry.size
|
35
|
+
end
|
36
|
+
@files.each do |file|
|
37
|
+
@size += ZippedIO::Deflater.new(file).size
|
38
|
+
end
|
39
|
+
@size += zip_central_directory.size
|
40
|
+
@size
|
41
|
+
end
|
42
|
+
|
43
|
+
def zip_central_directory
|
44
|
+
@zip_central_directory ||= ZipCentralDirectory.new(zip_entries)
|
45
|
+
end
|
46
|
+
|
47
|
+
def zip_entries
|
48
|
+
@zip_entries ||= @files.map { |file| ZipEntry.new(file) }
|
49
|
+
end
|
50
|
+
|
51
|
+
@@block_size = 4096
|
52
|
+
|
53
|
+
def self.block_size
|
54
|
+
@@block_size
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.block_size=(value)
|
58
|
+
@@block_size = value
|
59
|
+
end
|
60
|
+
|
61
|
+
class Deflater
|
62
|
+
|
63
|
+
attr_accessor :size
|
64
|
+
|
65
|
+
def initialize(file, level = 0)
|
66
|
+
@file = file
|
67
|
+
@zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
|
68
|
+
end
|
69
|
+
|
70
|
+
def read
|
71
|
+
@file.rewind
|
72
|
+
while data = @file.read(Harbor::ZippedIO::block_size)
|
73
|
+
yield @zlibDeflater.deflate(data)
|
74
|
+
end
|
75
|
+
until @zlibDeflater.finished?
|
76
|
+
yield @zlibDeflater.finish
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
def size
|
82
|
+
return @size if @size
|
83
|
+
@size = 0
|
84
|
+
@file.rewind
|
85
|
+
while data = @file.read(Harbor::ZippedIO::block_size)
|
86
|
+
@size += @zlibDeflater.deflate(data).size
|
87
|
+
end
|
88
|
+
|
89
|
+
until @zlibDeflater.finished?
|
90
|
+
@size += @zlibDeflater.finish.size
|
91
|
+
end
|
92
|
+
@size
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class ZipCentralDirectory
|
98
|
+
|
99
|
+
def initialize(entries)
|
100
|
+
@entries = entries
|
101
|
+
end
|
102
|
+
|
103
|
+
def read
|
104
|
+
generate unless @io
|
105
|
+
|
106
|
+
@io.pos = 0
|
107
|
+
while data = @io.read(Harbor::ZippedIO::block_size)
|
108
|
+
yield data
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def size
|
113
|
+
generate unless @io
|
114
|
+
@io.size
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def generate
|
120
|
+
@io = StringIO.new
|
121
|
+
@io << [
|
122
|
+
END_OF_CENTRAL_DIRECTORY_SIGNATURE,
|
123
|
+
0, # number of this disk
|
124
|
+
0, # numer of disk with start of central directory
|
125
|
+
@entries.size,
|
126
|
+
@entries.size,
|
127
|
+
@entries.inject(0) { |value, entry| entry.central_directory_header_size + value },
|
128
|
+
0, # offset
|
129
|
+
0 # comment length
|
130
|
+
].pack('VvvvvVVv')
|
131
|
+
@io << "" #comment
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
class ZipEntry
|
137
|
+
|
138
|
+
DEFLATED = 8
|
139
|
+
|
140
|
+
FSTYPE_UNIX = 3
|
141
|
+
|
142
|
+
LOCAL_ENTRY_SIGNATURE = 0x04034b50
|
143
|
+
CENTRAL_DIRECTORY_STATIC_HEADER_LENGTH = 46
|
144
|
+
|
145
|
+
attr_accessor :comment, :crc, :name, :size, :file
|
146
|
+
|
147
|
+
def initialize(file)
|
148
|
+
@file = file
|
149
|
+
|
150
|
+
@crc = 0
|
151
|
+
@compressed_size = file.size
|
152
|
+
@size = file.size
|
153
|
+
end
|
154
|
+
|
155
|
+
def binary_dos_date
|
156
|
+
(time.day) + (time.month << 5) + ((time.year - 1980) << 9)
|
157
|
+
end
|
158
|
+
|
159
|
+
def binary_dos_time
|
160
|
+
(time.sec / 2) + (time.min << 5) + (time.hour << 11)
|
161
|
+
end
|
162
|
+
|
163
|
+
def central_directory_header_size
|
164
|
+
CENTRAL_DIRECTORY_STATIC_HEADER_LENGTH + @file.name.size
|
165
|
+
end
|
166
|
+
|
167
|
+
def time
|
168
|
+
Time.now
|
169
|
+
end
|
170
|
+
|
171
|
+
def read_local_entry
|
172
|
+
generate unless @data
|
173
|
+
@data
|
174
|
+
end
|
175
|
+
|
176
|
+
def size
|
177
|
+
generate unless @data
|
178
|
+
@data.size
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def generate
|
184
|
+
@data ||= [
|
185
|
+
ZipEntry::LOCAL_ENTRY_SIGNATURE,
|
186
|
+
0,
|
187
|
+
0,
|
188
|
+
ZipEntry::DEFLATED,
|
189
|
+
binary_dos_time,
|
190
|
+
binary_dos_date,
|
191
|
+
@crc, #crc
|
192
|
+
@compressed_size,
|
193
|
+
@size,
|
194
|
+
@file.name.length,
|
195
|
+
0 # extra length
|
196
|
+
].pack('VvvvvvVVVvv') << @file.name << ""
|
197
|
+
end
|
198
|
+
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# == Cloud Files API
|
4
|
+
# ==== Connects Ruby Applications to Rackspace's {Mosso Cloud Files service}[http://www.mosso.com/cloudfiles.jsp]
|
5
|
+
# Initial work by Major Hayden <major.hayden@rackspace.com>
|
6
|
+
#
|
7
|
+
# Subsequent work by H. Wade Minter <wade.minter@rackspace.com>
|
8
|
+
#
|
9
|
+
# Copyright (C) 2008 Rackspace US, Inc.
|
10
|
+
#
|
11
|
+
# 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:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# 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.
|
16
|
+
#
|
17
|
+
# Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
18
|
+
#
|
19
|
+
# ----
|
20
|
+
#
|
21
|
+
# === Documentation & Examples
|
22
|
+
# To begin reviewing the available methods and examples, peruse the README file, or begin by looking at documentation for the
|
23
|
+
# CloudFiles::Connection class.
|
24
|
+
#
|
25
|
+
# The CloudFiles class is the base class. Not much of note happens here.
|
26
|
+
# To create a new CloudFiles connection, use the CloudFiles::Connection.new('user_name', 'api_key') method.
|
27
|
+
module CloudFiles
|
28
|
+
|
29
|
+
VERSION = '1.3.0'
|
30
|
+
require 'net/http'
|
31
|
+
require 'net/https'
|
32
|
+
require 'rexml/document'
|
33
|
+
require 'uri'
|
34
|
+
require 'digest/md5'
|
35
|
+
require 'jcode'
|
36
|
+
require 'time'
|
37
|
+
require 'rubygems'
|
38
|
+
require 'mime/types'
|
39
|
+
|
40
|
+
$KCODE = 'u'
|
41
|
+
|
42
|
+
$:.unshift(File.dirname(__FILE__))
|
43
|
+
require 'cloudfiles/authentication'
|
44
|
+
require 'cloudfiles/connection'
|
45
|
+
require 'cloudfiles/container'
|
46
|
+
require 'cloudfiles/storage_object'
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
class SyntaxException < StandardError # :nodoc:
|
53
|
+
end
|
54
|
+
class ConnectionException < StandardError # :nodoc:
|
55
|
+
end
|
56
|
+
class AuthenticationException < StandardError # :nodoc:
|
57
|
+
end
|
58
|
+
class InvalidResponseException < StandardError # :nodoc:
|
59
|
+
end
|
60
|
+
class NonEmptyContainerException < StandardError # :nodoc:
|
61
|
+
end
|
62
|
+
class NoSuchObjectException < StandardError # :nodoc:
|
63
|
+
end
|
64
|
+
class NoSuchContainerException < StandardError # :nodoc:
|
65
|
+
end
|
66
|
+
class NoSuchAccountException < StandardError # :nodoc:
|
67
|
+
end
|
68
|
+
class MisMatchedChecksumException < StandardError # :nodoc:
|
69
|
+
end
|
70
|
+
class IOException < StandardError # :nodoc:
|
71
|
+
end
|
72
|
+
class CDNNotEnabledException < StandardError # :nodoc:
|
73
|
+
end
|
74
|
+
class ObjectExistsException < StandardError # :nodoc:
|
75
|
+
end
|
76
|
+
class ExpiredAuthTokenException < StandardError # :nodoc:
|
77
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Copyright (C) 2008 Rackspace US, Inc.
|
2
|
+
#
|
3
|
+
# 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:
|
4
|
+
#
|
5
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
#
|
7
|
+
# 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.
|
8
|
+
#
|
9
|
+
# Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
10
|
+
|
11
|
+
module CloudFiles
|
12
|
+
class Authentication
|
13
|
+
|
14
|
+
# Performs an authentication to the Cloud Files servers. Opens a new HTTP connection to the API server,
|
15
|
+
# sends the credentials, and looks for a successful authentication. If it succeeds, it sets the cdmmgmthost,
|
16
|
+
# cdmmgmtpath, storagehost, storagepath, authtoken, and authok variables on the connection. If it fails, it raises
|
17
|
+
# an AuthenticationException.
|
18
|
+
#
|
19
|
+
# Should probably never be called directly.
|
20
|
+
def initialize(connection)
|
21
|
+
path = '/auth'
|
22
|
+
hdrhash = { "X-Auth-User" => connection.authuser, "X-Auth-Key" => connection.authkey }
|
23
|
+
begin
|
24
|
+
server = Net::HTTP.new('api.mosso.com',443)
|
25
|
+
server.use_ssl = true
|
26
|
+
server.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
27
|
+
server.start
|
28
|
+
rescue
|
29
|
+
raise ConnectionException, "Unable to connect to #{server}"
|
30
|
+
end
|
31
|
+
response = server.get(path,hdrhash)
|
32
|
+
if (response.code == "204")
|
33
|
+
connection.cdnmgmthost = URI.parse(response["x-cdn-management-url"]).host
|
34
|
+
connection.cdnmgmtpath = URI.parse(response["x-cdn-management-url"]).path
|
35
|
+
connection.storagehost = URI.parse(response["x-storage-url"]).host
|
36
|
+
connection.storagepath = URI.parse(response["x-storage-url"]).path
|
37
|
+
connection.authtoken = response["x-auth-token"]
|
38
|
+
connection.authok = true
|
39
|
+
else
|
40
|
+
connection.authtoken = false
|
41
|
+
raise AuthenticationException, "Authentication failed"
|
42
|
+
end
|
43
|
+
server.finish
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,280 @@
|
|
1
|
+
# Copyright (C) 2008 Rackspace US, Inc.
|
2
|
+
#
|
3
|
+
# 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:
|
4
|
+
#
|
5
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
#
|
7
|
+
# 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.
|
8
|
+
#
|
9
|
+
# Except as contained in this notice, the name of Rackspace US, Inc. shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization from Rackspace US, Inc.
|
10
|
+
|
11
|
+
module CloudFiles
|
12
|
+
class Connection
|
13
|
+
|
14
|
+
# Authentication key provided when the CloudFiles class was instantiated
|
15
|
+
attr_reader :authkey
|
16
|
+
|
17
|
+
# Token returned after a successful authentication
|
18
|
+
attr_accessor :authtoken
|
19
|
+
|
20
|
+
# Authentication username provided when the CloudFiles class was instantiated
|
21
|
+
attr_reader :authuser
|
22
|
+
|
23
|
+
# Hostname of the CDN management server
|
24
|
+
attr_accessor :cdnmgmthost
|
25
|
+
|
26
|
+
# Path for managing containers on the CDN management server
|
27
|
+
attr_accessor :cdnmgmtpath
|
28
|
+
|
29
|
+
# Array of requests that have been made so far
|
30
|
+
attr_reader :reqlog
|
31
|
+
|
32
|
+
# Hostname of the storage server
|
33
|
+
attr_accessor :storagehost
|
34
|
+
|
35
|
+
# Path for managing containers/objects on the storage server
|
36
|
+
attr_accessor :storagepath
|
37
|
+
|
38
|
+
# Instance variable that is set when authorization succeeds
|
39
|
+
attr_accessor :authok
|
40
|
+
|
41
|
+
# The total size in bytes under this connection
|
42
|
+
attr_reader :bytes
|
43
|
+
|
44
|
+
# The total number of containers under this connection
|
45
|
+
attr_reader :count
|
46
|
+
|
47
|
+
# Creates a new CloudFiles::Connection object. Uses CloudFiles::Authentication to perform the login for the connection.
|
48
|
+
# The authuser is the Mosso username, the authkey is the Mosso API key.
|
49
|
+
#
|
50
|
+
# Setting the optional retry_auth variable to false will cause an exception to be thrown if your authorization token expires.
|
51
|
+
# Otherwise, it will attempt to reauthenticate.
|
52
|
+
#
|
53
|
+
# This will likely be the base class for most operations.
|
54
|
+
#
|
55
|
+
# cf = CloudFiles::Connection.new(MY_USERNAME, MY_API_KEY)
|
56
|
+
def initialize(authuser,authkey,retry_auth = true)
|
57
|
+
@authuser = authuser
|
58
|
+
@authkey = authkey
|
59
|
+
@retry_auth = retry_auth
|
60
|
+
@authok = false
|
61
|
+
@http = {}
|
62
|
+
@reqlog = []
|
63
|
+
CloudFiles::Authentication.new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns true if the authentication was successful and returns false otherwise.
|
67
|
+
#
|
68
|
+
# cf.authok?
|
69
|
+
# => true
|
70
|
+
def authok?
|
71
|
+
@authok
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns an CloudFiles::Container object that can be manipulated easily. Throws a NoSuchContainerException if
|
75
|
+
# the container doesn't exist.
|
76
|
+
#
|
77
|
+
# container = cf.container('test')
|
78
|
+
# container.count
|
79
|
+
# => 2
|
80
|
+
def container(name)
|
81
|
+
CloudFiles::Container.new(self,name)
|
82
|
+
end
|
83
|
+
alias :get_container :container
|
84
|
+
|
85
|
+
# Sets instance variables for the bytes of storage used for this account/connection, as well as the number of containers
|
86
|
+
# stored under the account. Returns a hash with :bytes and :count keys, and also sets the instance variables.
|
87
|
+
#
|
88
|
+
# cf.get_info
|
89
|
+
# => {:count=>8, :bytes=>42438527}
|
90
|
+
# cf.bytes
|
91
|
+
# => 42438527
|
92
|
+
def get_info
|
93
|
+
response = cfreq("HEAD",@storagehost,@storagepath)
|
94
|
+
raise InvalidResponseException, "Unable to obtain account size" unless (response.code == "204")
|
95
|
+
@bytes = response["x-account-bytes-used"].to_i
|
96
|
+
@count = response["x-account-container-count"].to_i
|
97
|
+
{:bytes => @bytes, :count => @count}
|
98
|
+
end
|
99
|
+
|
100
|
+
# Gathers a list of the containers that exist for the account and returns the list of container names
|
101
|
+
# as an array. If no containers exist, an empty array is returned. Throws an InvalidResponseException
|
102
|
+
# if the request fails.
|
103
|
+
#
|
104
|
+
# If you supply the optional limit and marker parameters, the call will return the number of containers
|
105
|
+
# specified in limit, starting after the object named in marker.
|
106
|
+
#
|
107
|
+
# cf.containers
|
108
|
+
# => ["backup", "Books", "cftest", "test", "video", "webpics"]
|
109
|
+
#
|
110
|
+
# cf.containers(2,'cftest')
|
111
|
+
# => ["test", "video"]
|
112
|
+
def containers(limit=0,marker="")
|
113
|
+
paramarr = []
|
114
|
+
paramarr << ["limit=#{URI.encode(limit.to_s)}"] if limit.to_i > 0
|
115
|
+
paramarr << ["offset=#{URI.encode(marker.to_s)}"] unless marker.to_s.empty?
|
116
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
117
|
+
response = cfreq("GET",@storagehost,"#{@storagepath}?#{paramstr}")
|
118
|
+
return [] if (response.code == "204")
|
119
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
120
|
+
response.body.to_a.map { |x| x.chomp }
|
121
|
+
end
|
122
|
+
alias :list_containers :containers
|
123
|
+
|
124
|
+
# Retrieves a list of containers on the account along with their sizes (in bytes) and counts of the objects
|
125
|
+
# held within them. If no containers exist, an empty hash is returned. Throws an InvalidResponseException
|
126
|
+
# if the request fails.
|
127
|
+
#
|
128
|
+
# If you supply the optional limit and marker parameters, the call will return the number of containers
|
129
|
+
# specified in limit, starting after the object named in marker.
|
130
|
+
#
|
131
|
+
# cf.containers_detail
|
132
|
+
# => { "container1" => { :bytes => "36543", :count => "146" },
|
133
|
+
# "container2" => { :bytes => "105943", :count => "25" } }
|
134
|
+
def containers_detail(limit=0,marker="")
|
135
|
+
paramarr = []
|
136
|
+
paramarr << ["limit=#{URI.encode(limit.to_s)}"] if limit.to_i > 0
|
137
|
+
paramarr << ["offset=#{URI.encode(marker.to_s)}"] unless marker.to_s.empty?
|
138
|
+
paramstr = (paramarr.size > 0)? paramarr.join("&") : "" ;
|
139
|
+
response = cfreq("GET",@storagehost,"#{@storagepath}?format=xml&#{paramstr}")
|
140
|
+
return {} if (response.code == "204")
|
141
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
142
|
+
doc = REXML::Document.new(response.body)
|
143
|
+
detailhash = {}
|
144
|
+
doc.elements.each("account/container/") { |c|
|
145
|
+
detailhash[c.elements["name"].text] = { :bytes => c.elements["bytes"].text, :count => c.elements["count"].text }
|
146
|
+
}
|
147
|
+
doc = nil
|
148
|
+
return detailhash
|
149
|
+
end
|
150
|
+
alias :list_containers_info :containers_detail
|
151
|
+
|
152
|
+
# Returns true if the requested container exists and returns false otherwise.
|
153
|
+
#
|
154
|
+
# cf.container_exists?('good_container')
|
155
|
+
# => true
|
156
|
+
#
|
157
|
+
# cf.container_exists?('bad_container')
|
158
|
+
# => false
|
159
|
+
def container_exists?(containername)
|
160
|
+
response = cfreq("HEAD",@storagehost,"#{@storagepath}/#{containername}")
|
161
|
+
return (response.code == "204")? true : false ;
|
162
|
+
end
|
163
|
+
|
164
|
+
# Creates a new container and returns the CloudFiles::Container object. Throws an InvalidResponseException if the
|
165
|
+
# request fails.
|
166
|
+
#
|
167
|
+
# Slash (/) and question mark (?) are invalid characters, and will be stripped out. The container name is limited to
|
168
|
+
# 256 characters or less.
|
169
|
+
#
|
170
|
+
# container = cf.create_container('new_container')
|
171
|
+
# container.name
|
172
|
+
# => "new_container"
|
173
|
+
#
|
174
|
+
# container = cf.create_container('bad/name')
|
175
|
+
# => SyntaxException: Container name cannot contain the characters '/' or '?'
|
176
|
+
def create_container(containername)
|
177
|
+
raise SyntaxException, "Container name cannot contain the characters '/' or '?'" if containername.match(/[\/\?]/)
|
178
|
+
raise SyntaxException, "Container name is limited to 256 characters" if containername.length > 256
|
179
|
+
response = cfreq("PUT",@storagehost,"#{@storagepath}/#{containername}")
|
180
|
+
raise InvalidResponseException, "Unable to create container #{containername}" unless (response.code == "201" || response.code == "202")
|
181
|
+
CloudFiles::Container.new(self,containername)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Deletes a container from the account. Throws a NonEmptyContainerException if the container still contains
|
185
|
+
# objects. Throws a NoSuchContainerException if the container doesn't exist.
|
186
|
+
#
|
187
|
+
# cf.delete_container('new_container')
|
188
|
+
# => true
|
189
|
+
#
|
190
|
+
# cf.delete_container('video')
|
191
|
+
# => NonEmptyContainerException: Container video is not empty
|
192
|
+
#
|
193
|
+
# cf.delete_container('nonexistent')
|
194
|
+
# => NoSuchContainerException: Container nonexistent does not exist
|
195
|
+
def delete_container(containername)
|
196
|
+
response = cfreq("DELETE",@storagehost,"#{@storagepath}/#{containername}")
|
197
|
+
raise NonEmptyContainerException, "Container #{containername} is not empty" if (response.code == "409")
|
198
|
+
raise NoSuchContainerException, "Container #{containername} does not exist" unless (response.code == "204")
|
199
|
+
true
|
200
|
+
end
|
201
|
+
|
202
|
+
# Gathers a list of public (CDN-enabled) containers that exist for an account and returns the list of container names
|
203
|
+
# as an array. If no containers are public, an empty array is returned. Throws a InvalidResponseException if
|
204
|
+
# the request fails.
|
205
|
+
#
|
206
|
+
# If you pass the optional argument as true, it will only show containers that are CURRENTLY being shared on the CDN,
|
207
|
+
# as opposed to the default behavior which is to show all containers that have EVER been public.
|
208
|
+
#
|
209
|
+
# cf.public_containers
|
210
|
+
# => ["video", "webpics"]
|
211
|
+
def public_containers(enabled_only = false)
|
212
|
+
paramstr = enabled_only == true ? "enabled_only=true" : ""
|
213
|
+
response = cfreq("GET",@cdnmgmthost,"#{@cdnmgmtpath}?#{paramstr}")
|
214
|
+
return [] if (response.code == "204")
|
215
|
+
raise InvalidResponseException, "Invalid response code #{response.code}" unless (response.code == "200")
|
216
|
+
response.body.to_a.map { |x| x.chomp }
|
217
|
+
end
|
218
|
+
|
219
|
+
# This method actually makes the HTTP calls out to the server
|
220
|
+
def cfreq(method,server,path,headers = {},data = nil,attempts = 0,&block) # :nodoc:
|
221
|
+
start = Time.now
|
222
|
+
hdrhash = headerprep(headers)
|
223
|
+
path = URI.escape(path)
|
224
|
+
start_http(server,path,hdrhash)
|
225
|
+
request = Net::HTTP.const_get(method.to_s.capitalize).new(path,hdrhash)
|
226
|
+
if data
|
227
|
+
if data.respond_to?(:read)
|
228
|
+
request.body_stream = data
|
229
|
+
else
|
230
|
+
request.body = data
|
231
|
+
end
|
232
|
+
request.content_length = data.respond_to?(:lstat) ? data.stat.size : data.size
|
233
|
+
else
|
234
|
+
request.content_length = 0
|
235
|
+
end
|
236
|
+
response = @http[server].request(request,&block)
|
237
|
+
raise ExpiredAuthTokenException if response.code == "401"
|
238
|
+
response
|
239
|
+
rescue Errno::EPIPE, Timeout::Error, Errno::EINVAL, EOFError
|
240
|
+
# Server closed the connection, retry
|
241
|
+
raise ConnectionException, "Unable to reconnect to #{server} after #{count} attempts" if attempts >= 5
|
242
|
+
attempts += 1
|
243
|
+
@http[server].finish
|
244
|
+
start_http(server,path,headers)
|
245
|
+
retry
|
246
|
+
rescue ExpiredAuthTokenException
|
247
|
+
raise ConnectionException, "Authentication token expired and you have requested not to retry" if @retry_auth == false
|
248
|
+
CloudFiles::Authentication.new(self)
|
249
|
+
retry
|
250
|
+
end
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
# Sets up standard HTTP headers
|
255
|
+
def headerprep(headers = {}) # :nodoc:
|
256
|
+
default_headers = {}
|
257
|
+
default_headers["X-Auth-Token"] = @authtoken if (authok? && @account.nil?)
|
258
|
+
default_headers["X-Storage-Token"] = @authtoken if (authok? && !@account.nil?)
|
259
|
+
default_headers["Connection"] = "Keep-Alive"
|
260
|
+
default_headers["User-Agent"] = "Ruby-CloudFiles/#{VERSION}"
|
261
|
+
default_headers.merge(headers)
|
262
|
+
end
|
263
|
+
|
264
|
+
# Starts (or restarts) the HTTP connection
|
265
|
+
def start_http(server,path,headers) # :nodoc:
|
266
|
+
if (@http[server].nil?)
|
267
|
+
begin
|
268
|
+
@http[server] = Net::HTTP.new(server,443)
|
269
|
+
@http[server].use_ssl = true
|
270
|
+
@http[server].verify_mode = OpenSSL::SSL::VERIFY_NONE
|
271
|
+
@http[server].start
|
272
|
+
rescue
|
273
|
+
raise ConnectionException, "Unable to connect to #{server}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|