smugmug 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/HISTORY +2 -0
- data/LICENSE +19 -0
- data/MANIFEST +48 -0
- data/README +30 -0
- data/Rakefile +100 -0
- data/bin/smcli +225 -0
- data/bin/smugmug2sql +158 -0
- data/doc/API +310 -0
- data/doc/TODO +32 -0
- data/lib/net/httpz.rb +31 -0
- data/lib/smugmug.rb +179 -0
- data/lib/smugmug/album/info.rb +131 -0
- data/lib/smugmug/album/stats.rb +31 -0
- data/lib/smugmug/albums.rb +39 -0
- data/lib/smugmug/base.rb +104 -0
- data/lib/smugmug/cache.rb +33 -0
- data/lib/smugmug/config.rb +48 -0
- data/lib/smugmug/image/exif.rb +72 -0
- data/lib/smugmug/image/info.rb +88 -0
- data/lib/smugmug/image/stats.rb +32 -0
- data/lib/smugmug/images.rb +52 -0
- data/lib/smugmug/table.rb +133 -0
- data/lib/smugmug/util.rb +12 -0
- data/test/album.rb +359 -0
- data/test/config.rb +39 -0
- data/test/httpz.rb +120 -0
- data/test/image.rb +540 -0
- data/test/login.rb +24 -0
- data/test/runner.rb +83 -0
- data/test/servlet.rb +257 -0
- data/test/table.rb +113 -0
- data/xml/canned +212 -0
- data/xml/fail/empty.set.xml +4 -0
- data/xml/fail/invalid.apikey.xml +4 -0
- data/xml/fail/invalid.login.xml +4 -0
- data/xml/fail/invalid.method.xml +4 -0
- data/xml/fail/invalid.user.xml +4 -0
- data/xml/fail/system.error.xml +4 -0
- data/xml/standard/albums.get.xml +24 -0
- data/xml/standard/albums.getInfo.xml +38 -0
- data/xml/standard/albums.getStats.xml +43 -0
- data/xml/standard/categories.get.xml +213 -0
- data/xml/standard/images.get.xml +9 -0
- data/xml/standard/images.getEXIF.xml +34 -0
- data/xml/standard/images.getInfo.xml +29 -0
- data/xml/standard/images.getStats.xml +15 -0
- data/xml/standard/login.withHash.xml +7 -0
- data/xml/standard/login.withPassword.xml +10 -0
- metadata +103 -0
data/test/login.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# $Hg$
|
3
|
+
|
4
|
+
require 'test/unit'
|
5
|
+
require 'smugmug'
|
6
|
+
|
7
|
+
class TC_Login < Test::Unit::TestCase
|
8
|
+
def teardown
|
9
|
+
File.unlink(ENV['SMUGMUG_RC'])
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_login
|
13
|
+
sm = nil
|
14
|
+
|
15
|
+
assert_nil(sm)
|
16
|
+
assert_raise(SmugMug::SmugMugError) { SmugMug::SmugMug.new('foo', 'blank') }
|
17
|
+
assert_nothing_raised { SmugMug::SmugMug.new('smugmug@example.net', 'secret') }
|
18
|
+
|
19
|
+
assert_nothing_raised { sm = SmugMug::SmugMug.new }
|
20
|
+
assert_equal('http://localhost:45556/', sm.base)
|
21
|
+
|
22
|
+
assert_not_nil(sm)
|
23
|
+
end
|
24
|
+
end
|
data/test/runner.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# $Hg$
|
3
|
+
|
4
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
5
|
+
require 'test/unit/ui/console/testrunner'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'logger'
|
8
|
+
require 'servlet'
|
9
|
+
require 'smugmug'
|
10
|
+
|
11
|
+
if not (File.exists?('table.rb'))
|
12
|
+
raise 'The test suite should only be run from the test directory.'
|
13
|
+
end
|
14
|
+
|
15
|
+
%w{access.log servlet.log test.log}.each do |filename|
|
16
|
+
File.unlink(filename) if File.exists?(filename)
|
17
|
+
end
|
18
|
+
|
19
|
+
ENV['SMUGMUG_URL'] = 'http://localhost:45556/'
|
20
|
+
ENV['SMUGMUG_RC'] = '.smugmugrc'
|
21
|
+
|
22
|
+
## Imitation smugmug.com
|
23
|
+
server = WEBrick::HTTPServer.new(
|
24
|
+
:BindAddress => '127.0.0.1',
|
25
|
+
:Port => 45556,
|
26
|
+
:AccessLog => [
|
27
|
+
[ File.open('access.log', 'w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT ]
|
28
|
+
],
|
29
|
+
:Logger => WEBrick::Log.new('servlet.log')
|
30
|
+
)
|
31
|
+
|
32
|
+
threads = []
|
33
|
+
threads << Thread.new do
|
34
|
+
server.mount('/', SmugMugServlet)
|
35
|
+
server.start
|
36
|
+
end
|
37
|
+
|
38
|
+
%w{INT TERM EXIT}.each do |sig|
|
39
|
+
trap(sig) do
|
40
|
+
server.shutdown
|
41
|
+
threads.each {|t| t.join}
|
42
|
+
File.unlink('.smugmugrc') if File.exists?('.smugmugrc')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
$log = Logger.new('test.log')
|
47
|
+
$smugmug = SmugMug::SmugMug.new('smugmug@example.net', 'secret')
|
48
|
+
File.unlink(ENV['SMUGMUG_RC'])
|
49
|
+
|
50
|
+
if ARGV.any?
|
51
|
+
ARGV.each {|fn| require(fn) }
|
52
|
+
else
|
53
|
+
## Stand alone
|
54
|
+
require('config.rb')
|
55
|
+
require('httpz.rb')
|
56
|
+
require('table.rb')
|
57
|
+
|
58
|
+
## SmugMug.com WEBrick
|
59
|
+
|
60
|
+
require('login.rb')
|
61
|
+
require('album.rb')
|
62
|
+
require('image.rb')
|
63
|
+
end
|
64
|
+
|
65
|
+
class SmugMugTestSuite < Test::Unit::TestSuite
|
66
|
+
def initialize
|
67
|
+
super('SmugMug')
|
68
|
+
end
|
69
|
+
def self.suite
|
70
|
+
suite = Test::Unit::TestSuite.new
|
71
|
+
ObjectSpace.each_object(Class) do |klass|
|
72
|
+
suite << klass.suite if (Test::Unit::TestCase > klass)
|
73
|
+
end
|
74
|
+
return suite
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
passed = Test::Unit::UI::Console::TestRunner.run(SmugMugTestSuite.suite).passed?
|
79
|
+
Kernel.exit(passed ? 0 : 1)
|
80
|
+
|
81
|
+
# Local Variables:
|
82
|
+
# ruby-indent-level: 4
|
83
|
+
# End:
|
data/test/servlet.rb
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# -*- Mode: ruby; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
3
|
+
# $Hg: servlet.rb,v 3b8b5b808b51 2007/08/11 07:33:22 boumenot $
|
4
|
+
|
5
|
+
require 'webrick'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
module URI
|
9
|
+
class Generic
|
10
|
+
def [](key)
|
11
|
+
return query_hash[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def query_hash
|
15
|
+
raise RuntimeError if query.nil?
|
16
|
+
hash = {}
|
17
|
+
query.split('&').each do |s|
|
18
|
+
k,v = s.split('=')
|
19
|
+
hash[k] = v
|
20
|
+
end
|
21
|
+
return hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_key?(key)
|
25
|
+
return query_hash.has_key?(key)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class EmptySet < RuntimeError; end
|
31
|
+
class InvalidAPIKey < RuntimeError; end
|
32
|
+
class InvalidLogin < RuntimeError; end
|
33
|
+
class InvalidMethod < RuntimeError; end
|
34
|
+
class InvalidUser < RuntimeError; end
|
35
|
+
class SystemError < RuntimeError; end
|
36
|
+
|
37
|
+
class Methods
|
38
|
+
attr_accessor :uri
|
39
|
+
|
40
|
+
attr_reader :account_type, :logger
|
41
|
+
attr_reader :api_key, :email_address, :password, :password_hash, :session_id, :user_id
|
42
|
+
|
43
|
+
@authenticated = false
|
44
|
+
class << self
|
45
|
+
attr_accessor :authenticated
|
46
|
+
def authenticated?() @authenticated ; end
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(account_type='Standard')
|
50
|
+
@authenticated = false
|
51
|
+
|
52
|
+
@account_type = account_type
|
53
|
+
@api_key = 'dydlCYDzmykqoLkVGgECEIP5WEuwWFzc'
|
54
|
+
@email_address = 'smugmug@example.net'
|
55
|
+
@password = 'secret'
|
56
|
+
@password_hash = '$1$add02c7c2232759874e1c205587017bed'
|
57
|
+
@session_id = '42e74d96e8a3454259eed4e2b6a143cf'
|
58
|
+
@user_id = 1001
|
59
|
+
end
|
60
|
+
|
61
|
+
## accessors
|
62
|
+
|
63
|
+
def authenticated?() self.class.authenticated? ; end
|
64
|
+
|
65
|
+
## methods
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def api_key?() uri['APIKey'] == api_key ; end
|
70
|
+
def email_address?() uri['EmailAddress'] == email_address ; end
|
71
|
+
def password?() uri['Password'] == password ; end
|
72
|
+
def password_hash?() uri['PasswordHash'] == password_hash ; end
|
73
|
+
def session_id?() uri['SessionID'] == session_id ; end
|
74
|
+
def user_id?() uri['UserID'].to_i == user_id ; end
|
75
|
+
|
76
|
+
def login_check
|
77
|
+
raise InvalidAPIKey unless uri.has_key?('APIKey')
|
78
|
+
raise InvalidAPIKey unless api_key?
|
79
|
+
end
|
80
|
+
|
81
|
+
public
|
82
|
+
|
83
|
+
## login
|
84
|
+
|
85
|
+
def login?()
|
86
|
+
return %w{smugmug.login.withPassword smugmug.login.withHash}.include?(uri['method'])
|
87
|
+
end
|
88
|
+
|
89
|
+
def login_withPassword
|
90
|
+
raise InvalidLogin unless uri.has_key?('EmailAddress')
|
91
|
+
raise InvalidLogin unless uri.has_key?('Password')
|
92
|
+
|
93
|
+
raise InvalidLogin unless email_address?
|
94
|
+
raise InvalidLogin unless password?
|
95
|
+
|
96
|
+
login_check()
|
97
|
+
|
98
|
+
self.class.authenticated = true
|
99
|
+
return _read('login.withPassword')
|
100
|
+
end
|
101
|
+
|
102
|
+
def login_withHash
|
103
|
+
raise InvalidLogin unless uri.has_key?('UserID')
|
104
|
+
raise InvalidLogin unless uri.has_key?('PasswordHash')
|
105
|
+
|
106
|
+
raise InvalidLogin unless user_id?
|
107
|
+
raise InvalidLogin unless password_hash?
|
108
|
+
|
109
|
+
login_check()
|
110
|
+
|
111
|
+
self.class.authenticated = true
|
112
|
+
return _read('login.withHash')
|
113
|
+
end
|
114
|
+
|
115
|
+
## albums
|
116
|
+
|
117
|
+
def album_id?()
|
118
|
+
return (uri['AlbumID'].to_i >= 1001 and uri['AlbumID'].to_i <= 1003)
|
119
|
+
end
|
120
|
+
|
121
|
+
def albums?()
|
122
|
+
raise InvalidUser unless uri.has_key?('AlbumID')
|
123
|
+
raise InvalidUser unless album_id?
|
124
|
+
end
|
125
|
+
|
126
|
+
def albums_get
|
127
|
+
raise InvalidAPIKey unless session_id?
|
128
|
+
return _read('albums.get')
|
129
|
+
end
|
130
|
+
def albums_getInfo
|
131
|
+
raise InvalidAPIKey unless session_id?
|
132
|
+
raise SystemError unless uri.has_key?('AlbumID')
|
133
|
+
albums?
|
134
|
+
return _read('albums.getInfo')
|
135
|
+
end
|
136
|
+
def albums_getStats
|
137
|
+
raise InvalidAPIKey unless session_id?
|
138
|
+
albums?
|
139
|
+
return _read('albums.getStats')
|
140
|
+
end
|
141
|
+
|
142
|
+
## images
|
143
|
+
|
144
|
+
def image_id?()
|
145
|
+
return (uri['ImageID'].to_i >= 1001 and uri['ImageID'].to_i <= 1003)
|
146
|
+
end
|
147
|
+
|
148
|
+
def images?()
|
149
|
+
raise InvalidUser unless uri.has_key?('ImageID')
|
150
|
+
raise InvalidUser unless image_id?
|
151
|
+
end
|
152
|
+
|
153
|
+
def images_get
|
154
|
+
albums?
|
155
|
+
raise InvalidAPIKey unless session_id?
|
156
|
+
return _read('images.get')
|
157
|
+
end
|
158
|
+
def images_getInfo
|
159
|
+
raise InvalidAPIKey unless session_id?
|
160
|
+
images?
|
161
|
+
return _read('images.getInfo')
|
162
|
+
end
|
163
|
+
def images_getEXIF
|
164
|
+
raise InvalidAPIKey unless session_id?
|
165
|
+
images?
|
166
|
+
return _read('images.getEXIF')
|
167
|
+
end
|
168
|
+
def images_getStats
|
169
|
+
raise InvalidAPIKey unless session_id?
|
170
|
+
images?
|
171
|
+
return _read('images.getStats')
|
172
|
+
end
|
173
|
+
|
174
|
+
## fail
|
175
|
+
|
176
|
+
def empty_set
|
177
|
+
return _read('empty_set', 'fail')
|
178
|
+
end
|
179
|
+
def invalid_login
|
180
|
+
return _read('invalid_login', 'fail')
|
181
|
+
end
|
182
|
+
def invalid_method
|
183
|
+
return _read('invalid_method', 'fail')
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def _read(method, type=account_type.downcase)
|
189
|
+
method.gsub!('smugmug.', '')
|
190
|
+
method.gsub!('_', '.')
|
191
|
+
|
192
|
+
filename = File.join('..', 'xml', type, "#{method}.xml")
|
193
|
+
unless File.exists?(filename)
|
194
|
+
raise RuntimeError, "Cannot locate the XML file (#{filename} )for the method #{method}!"
|
195
|
+
end
|
196
|
+
return IO.read(filename)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class SmugMugServlet < WEBrick::HTTPServlet::AbstractServlet
|
201
|
+
attr_reader :method
|
202
|
+
def initialize(*args)
|
203
|
+
super(*args)
|
204
|
+
@method = Methods.new
|
205
|
+
end
|
206
|
+
|
207
|
+
def _method
|
208
|
+
raise InvalidMethod unless @method.uri.has_key?('method')
|
209
|
+
m = @method.uri['method']
|
210
|
+
m.gsub!('smugmug.', '')
|
211
|
+
m.gsub!('.', '_')
|
212
|
+
return m.to_sym
|
213
|
+
end
|
214
|
+
|
215
|
+
def do_GET(req, res)
|
216
|
+
@logger.info("URI: " + req.request_uri.to_s)
|
217
|
+
begin
|
218
|
+
@method.uri = req.request_uri
|
219
|
+
raise InvalidMethod.new unless method.respond_to?(_method)
|
220
|
+
|
221
|
+
unless method.authenticated?
|
222
|
+
raise InvalidAPIKey unless method.login?
|
223
|
+
end
|
224
|
+
|
225
|
+
res.body = method.send(_method)
|
226
|
+
rescue InvalidLogin
|
227
|
+
raise WEBrick::HTTPStatus::Unauthorized, method.invalid_login
|
228
|
+
rescue InvalidMethod
|
229
|
+
raise WEBrick::HTTPStatus::MethodNotAllowed, method.invalid_method
|
230
|
+
rescue
|
231
|
+
@logger.info($!.class)
|
232
|
+
$!.backtrace.each { |line| @logger.info(line) }
|
233
|
+
raise WEBrick::HTTPStatus::InternalServerError
|
234
|
+
end
|
235
|
+
|
236
|
+
return res
|
237
|
+
end
|
238
|
+
|
239
|
+
def do_PUT
|
240
|
+
end
|
241
|
+
|
242
|
+
def do_POST
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
if __FILE__ == $0
|
247
|
+
s = WEBrick::HTTPServer.new(
|
248
|
+
:BindAddress => '127.0.0.1',
|
249
|
+
:Port => 45556,
|
250
|
+
:Logger => WEBrick::Log.new('servlet.log', WEBrick::Log::DEBUG),
|
251
|
+
:AccessLog => [ [ File.open('access.log', 'w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT ] ]
|
252
|
+
)
|
253
|
+
s.mount('/', SmugMugServlet)
|
254
|
+
trap("INT") { s.shutdown }
|
255
|
+
trap("TERM") { s.shutdown }
|
256
|
+
s.start
|
257
|
+
end
|
data/test/table.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# -*- ruby -*-
|
2
|
+
# $Hg: table.rb,v 1a7959f46f5c 2007/08/11 05:56:18 boumenot $
|
3
|
+
|
4
|
+
require 'test/unit'
|
5
|
+
require 'smugmug/table'
|
6
|
+
|
7
|
+
|
8
|
+
class TC_MethodTable < Test::Unit::TestCase
|
9
|
+
attr_reader :std, :pow, :poW, :pro, :sup
|
10
|
+
|
11
|
+
def setup
|
12
|
+
table_xml = %q{
|
13
|
+
<smugmug>
|
14
|
+
<TestId type="int" xpath="//TestId/attribute::id"/>
|
15
|
+
<String/>
|
16
|
+
<Int type="int"/>
|
17
|
+
<False type="boolean"/>
|
18
|
+
<True type="boolean"/>
|
19
|
+
<Power account="power"/>
|
20
|
+
<Pro account="pro"/>
|
21
|
+
</smugmug>
|
22
|
+
}
|
23
|
+
|
24
|
+
resp_xml = %q{
|
25
|
+
<test>
|
26
|
+
<TestId id="1234"/>
|
27
|
+
<String>foo</String>
|
28
|
+
<Int>4567</Int>
|
29
|
+
<False>0</False>
|
30
|
+
<True>1</True>
|
31
|
+
<Power>power</Power>
|
32
|
+
<Pro>pro</Pro>
|
33
|
+
</test>
|
34
|
+
}
|
35
|
+
|
36
|
+
@doc = REXML::Document.new(resp_xml)
|
37
|
+
|
38
|
+
@std = SmugMug::MethodTable.new(table_xml, 'standard', 'foo')
|
39
|
+
@pow = SmugMug::MethodTable.new(table_xml, 'power', 'foo')
|
40
|
+
@poW = SmugMug::MethodTable.new(table_xml, 'PoWeR', 'foo')
|
41
|
+
@pro = SmugMug::MethodTable.new(table_xml, 'pro', 'foo')
|
42
|
+
@sup = SmugMug::MethodTable.new(table_xml, 'super', 'foo')
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_klass
|
46
|
+
%w{std pow poW pro sup}.each do |inst|
|
47
|
+
assert_equal('foo', self.send(inst.to_sym).klass)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_bad_permission
|
52
|
+
assert_nothing_raised { std.get(@doc, :String) }
|
53
|
+
assert_nothing_raised { pow.get(@doc, :String) }
|
54
|
+
assert_nothing_raised { pro.get(@doc, :String) }
|
55
|
+
|
56
|
+
assert_raise(RuntimeError) { std.get(@doc, :Power) }
|
57
|
+
assert_raise(RuntimeError) { std.get(@doc, :Pro) }
|
58
|
+
assert_raise(RuntimeError) { pow.get(@doc, :Pro) }
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_convert_to_type
|
62
|
+
assert_instance_of String, std.get(@doc, :String)
|
63
|
+
assert_instance_of Fixnum, std.get(@doc, :Int)
|
64
|
+
assert_instance_of Fixnum, std.get(@doc, :TestId)
|
65
|
+
assert_instance_of FalseClass, std.get(@doc, :False)
|
66
|
+
assert_instance_of TrueClass, std.get(@doc, :True)
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_mixed_case_account_type
|
70
|
+
assert_nothing_raised { poW.get(@doc, :String) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_return_values
|
74
|
+
%w{std pow poW pro}.each do |inst|
|
75
|
+
assert_equal 1234, self.send(inst.to_sym).get(@doc, :TestId)
|
76
|
+
assert_equal 'foo', self.send(inst.to_sym).get(@doc, :String)
|
77
|
+
assert_equal 4567, self.send(inst.to_sym).get(@doc, :Int)
|
78
|
+
assert_equal false, self.send(inst.to_sym).get(@doc, :False)
|
79
|
+
assert_equal true, self.send(inst.to_sym).get(@doc, :True)
|
80
|
+
end
|
81
|
+
|
82
|
+
assert_equal 'power', pow.get(@doc, :Power)
|
83
|
+
assert_equal 'power', poW.get(@doc, :Power)
|
84
|
+
assert_equal 'power', pro.get(@doc, :Power)
|
85
|
+
assert_equal 'pro', pro.get(@doc, :Pro)
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_undefined_methods
|
89
|
+
assert_raise(NoMethodError) { std.get(@doc, :fart) }
|
90
|
+
assert_raise(NoMethodError) { pow.get(@doc, :fart) }
|
91
|
+
assert_raise(NoMethodError) { pro.get(@doc, :fart) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_unknown_account_type
|
95
|
+
assert_raise(RuntimeError) { sup.get(@doc, :Power) }
|
96
|
+
assert_raise(RuntimeError) { sup.get(@doc, :Pro) }
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_aliases
|
100
|
+
%w{std pow poW pro}.each do |inst|
|
101
|
+
assert_equal 1234, self.send(inst.to_sym).get(@doc, :test_id)
|
102
|
+
assert_equal 'foo', self.send(inst.to_sym).get(@doc, :string)
|
103
|
+
assert_equal 4567, self.send(inst.to_sym).get(@doc, :int)
|
104
|
+
assert_equal false, self.send(inst.to_sym).get(@doc, :false?)
|
105
|
+
assert_equal true, self.send(inst.to_sym).get(@doc, :true?)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Local Variables:
|
111
|
+
# ruby-indent-level: 4
|
112
|
+
# End:
|
113
|
+
|