smugmug 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|