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.
Files changed (49) hide show
  1. data/HISTORY +2 -0
  2. data/LICENSE +19 -0
  3. data/MANIFEST +48 -0
  4. data/README +30 -0
  5. data/Rakefile +100 -0
  6. data/bin/smcli +225 -0
  7. data/bin/smugmug2sql +158 -0
  8. data/doc/API +310 -0
  9. data/doc/TODO +32 -0
  10. data/lib/net/httpz.rb +31 -0
  11. data/lib/smugmug.rb +179 -0
  12. data/lib/smugmug/album/info.rb +131 -0
  13. data/lib/smugmug/album/stats.rb +31 -0
  14. data/lib/smugmug/albums.rb +39 -0
  15. data/lib/smugmug/base.rb +104 -0
  16. data/lib/smugmug/cache.rb +33 -0
  17. data/lib/smugmug/config.rb +48 -0
  18. data/lib/smugmug/image/exif.rb +72 -0
  19. data/lib/smugmug/image/info.rb +88 -0
  20. data/lib/smugmug/image/stats.rb +32 -0
  21. data/lib/smugmug/images.rb +52 -0
  22. data/lib/smugmug/table.rb +133 -0
  23. data/lib/smugmug/util.rb +12 -0
  24. data/test/album.rb +359 -0
  25. data/test/config.rb +39 -0
  26. data/test/httpz.rb +120 -0
  27. data/test/image.rb +540 -0
  28. data/test/login.rb +24 -0
  29. data/test/runner.rb +83 -0
  30. data/test/servlet.rb +257 -0
  31. data/test/table.rb +113 -0
  32. data/xml/canned +212 -0
  33. data/xml/fail/empty.set.xml +4 -0
  34. data/xml/fail/invalid.apikey.xml +4 -0
  35. data/xml/fail/invalid.login.xml +4 -0
  36. data/xml/fail/invalid.method.xml +4 -0
  37. data/xml/fail/invalid.user.xml +4 -0
  38. data/xml/fail/system.error.xml +4 -0
  39. data/xml/standard/albums.get.xml +24 -0
  40. data/xml/standard/albums.getInfo.xml +38 -0
  41. data/xml/standard/albums.getStats.xml +43 -0
  42. data/xml/standard/categories.get.xml +213 -0
  43. data/xml/standard/images.get.xml +9 -0
  44. data/xml/standard/images.getEXIF.xml +34 -0
  45. data/xml/standard/images.getInfo.xml +29 -0
  46. data/xml/standard/images.getStats.xml +15 -0
  47. data/xml/standard/login.withHash.xml +7 -0
  48. data/xml/standard/login.withPassword.xml +10 -0
  49. 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
+