ruby-smugmug 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Placester, Inc
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,37 @@
1
+ Overview
2
+ ===
3
+ Library for interacting with the SmugMug 1.3.0 API using OAuth authentication. This does not do any OAuth authorization or setup, it's assumed you're using another gem such as omniauth-oauth that handles that part.
4
+
5
+ Compability
6
+ -
7
+ Tested against Ruby 1.8.7, 1.9.2, 2.0.0 and JRuby, build history is available [here](http://travis-ci.org/zanker/ruby-smugmug).
8
+
9
+ <img src="https://secure.travis-ci.org/zanker/ruby-smugmug.png?branch=master&.png"/>
10
+
11
+ Examples
12
+ -
13
+ This is just a thin wrapper around the [SmugMug 1.3.0 API](http://wiki.smugmug.net/display/API/API+1.3.0), it's a 1:1 wrapper, so all of the documentation on the SmugMug page applies to this library. You can use any arguments that the SmugMug 1.3.0 documentation shows under the OAuth option.
14
+
15
+ client = SmugMug::Client.new(:api_key => "1234-api", :oauth_secret => "4321-secret", :user => {:token => "abcd-token", :secret => "abcd-secret"})
16
+
17
+ data = client.users.getStats(:Month => 2, :Year => 2012)
18
+ puts data # {"Bytes"=>0, "Hits"=>0, "Large"=>0, "Medium"=>0, "Small"=>0, "Video110"=>0, "Video200"=>0, "Video320"=>0, "Video640"=>0, "X2Large"=>0, "X3Large"=>0, "XLarge"=>0}
19
+
20
+ data = client.styles.getTemplates
21
+ puts data # [{"id"=>0, "Name"=>"Viewer Controlled"}, {"id"=>3, "Name"=>"SmugMug"}, {"id"=>4, "Name"=>"Traditional"}, {"id"=>7, "Name"=>"All Thumbs"}, {"id"=>8, "Name"=>"Slideshow"}, {"id"=>9, "Name"=>"Journal (Old)"}, {"id"=>10, "Name"=>"SmugMug Small"}, {"id"=>11, "Name"=>"Filmstrip"}, {"id"=>12, "Name"=>"Critique"}, {"id"=>16, "Name"=>"Journal"}, {"id"=>17, "Name"=>"Thumbnails"}]
22
+
23
+
24
+ Because uploading is a special case and not under the same group of APIs, it's called slightly differently. See http://wiki.smugmug.net/display/API/Uploading for more information or the documentation linked below for a list of arguments.
25
+
26
+ client = SmugMug::Client.new(:api_key => "1234-api", :oauth_secret => "4321-secret", :user => {:token => "abcd-token", :secret => "abcd-secret"})
27
+ data = client.upload_media(:file => File.new("/Users/foobar/Desktop/image.jpeg", :AlbumID => 51343)
28
+ puts data # {"id"=>1970029991, "Key"=>"rnSfAak", "URL"=>"http://foobar.smugmug.com/Other/Foo/51343_k8W1aR#1970029991_rnSfAak"}
29
+
30
+
31
+ Documentation
32
+ -
33
+ See http://rubydoc.info/github/zanker/ruby-smugmug/master/frames for full documentation.
34
+
35
+ License
36
+ -
37
+ Available under the MIT license, see LICENSE for more information.
@@ -0,0 +1,12 @@
1
+ require "bundler"
2
+ Bundler.setup
3
+
4
+ require "rake"
5
+ require "rspec"
6
+ require "rspec/core/rake_task"
7
+
8
+ RSpec::Core::RakeTask.new("spec") do |spec|
9
+ spec.pattern = "spec/**/*_spec.rb"
10
+ end
11
+
12
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ path = File.expand_path("../smugmug", __FILE__)
2
+ require "#{path}/version"
3
+ require "#{path}/exceptions"
4
+ require "#{path}/http"
5
+ require "#{path}/api_methods"
6
+ require "#{path}/api_category"
7
+ require "#{path}/client"
8
+
@@ -0,0 +1,12 @@
1
+ module SmugMug
2
+ class ApiCategory
3
+ def initialize(http, category)
4
+ @http, @category = http, category
5
+ end
6
+
7
+ def method_missing(method, *args)
8
+ return super unless SmugMug::API_METHODS[@category][method.to_s]
9
+ @http.request("#{@category}.#{method}", args.pop || {})
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,18 @@
1
+ module SmugMug
2
+ API_METHODS = {
3
+ "accounts" => {"browse" => true},
4
+ "albums" => {"applyWatermark" => true, "browse" => true, "changeSettings" => true, "comments" => true, "create" => true, "delete" => true, "get" => true, "getInfo" => true, "getStats" => true, "removeWatermark" => true, "reSort" => true},
5
+ "albumtemplates" => {"changeSettings" => true, "create" => true, "delete" => true, "get" => true}, "auth" => {"checkAccessToken" => true, "getAccessToken" => true, "getRequestToken" => true},
6
+ "categories" => {"create" => true, "delete" => true, "get" => true, "rename" => true}, "communities" => {"get" => true}, "coupons" => {"create" => true, "get" => true, "getInfo" => true, "modify" => true, "restrictions" => true},
7
+ "family" => {"add" => true, "get" => true, "remove" => true, "removeAll" => true}, "fans" => {"get" => true}, "featured" => {"albums" => true}, "friends" => {"add" => true, "get" => true, "remove" => true, "removeAll" => true},
8
+ "images" => {"applyWatermark" => true, "changePosition" => true, "changeSettings" => true, "collect" => true, "comments" => true, "crop" => true, "delete" => true, "get" => true, "getEXIF" => true, "getInfo" => true, "getStats" => true, "getURLs" => true, "removeWatermark" => true, "rotate" => true, "uploadFromURL" => true, "zoomThumbnail" => true},
9
+ "printmarks" => {"create" => true, "delete" => true, "get" => true, "getInfo" => true, "modify" => true},
10
+ "service" => {"ping" => true},
11
+ "sharegroups" => {"albums" => true, "browse" => true, "create" => true, "delete" => true, "get" => true, "getInfo" => true, "modify" => true},
12
+ "styles" => {"getTemplates" => true},
13
+ "subcategories" => {"create" => true, "delete" => true, "get" => true, "getAll" => true, "rename" => true},
14
+ "themes" => {"get" => true},
15
+ "users" => {"getInfo" => true, "getStats" => true, "getTree" => true},
16
+ "watermarks" => {"changeSettings" => true, "create" => true, "delete" => true, "get" => true, "getInfo" => true}
17
+ }
18
+ end
@@ -0,0 +1,85 @@
1
+ module SmugMug
2
+ class Client
3
+ ##
4
+ # Creates a new client instance that can be used to access the SMugMug API
5
+ # @param [Hash] args
6
+ # @option args [String] :api_key Your SmugMug API key
7
+ # @option args [String] :oauth_secret Your SmugMug OAuth secret key
8
+ # @option args [Hash] :user Configuration for the user you are requesting/updating data for
9
+ # * :token [String] OAuth token for the user
10
+ # * :secret [String] OAuth secret token for the user
11
+ # @option args [String, Optional :user_agent Helps SmugMug identify API calls
12
+ # @option args [Hash, Optional] :http Additional configuration for the HTTP requests
13
+ # * :verify_mode [Integer, Optional] How to verify the SSL certificate when connecting through HTTPS, either OpenSSL::SSL::VERIFY_PEER or OpenSSL::SSL::VERIFY_NONE, defaults to OpenSSL::SSL::VERIFY_NONE
14
+ # * :ca_file [String, Optional] Path to the CA certification file in PEM format
15
+ # * :ca_path [String, Optional] Path to the directory containing CA certifications in PEM format
16
+ #
17
+ # @raise [ArgumentError]
18
+ def initialize(args)
19
+ raise ArgumentError, "API Key required" unless args.has_key?(:api_key)
20
+ raise ArgumentError, "API OAuth secret required" unless args.has_key?(:oauth_secret)
21
+ raise ArgumentError, "Must specify the users OAuth datA" unless args[:user].is_a?(Hash)
22
+ raise ArgumentError, "Users OAuth token required" unless args[:user][:token]
23
+ raise ArgumentError, "Users OAuth secret token required" unless args[:user][:secret]
24
+
25
+ @http = HTTP.new(args)
26
+ end
27
+
28
+ ##
29
+ # Uploading media files to SmugMug, see http://wiki.smugmug.net/display/API/Uploading for more information
30
+ # @param [Hash] args
31
+ # @option args [File] :file File or stream that can have the content read to upload
32
+ # @option args [String] :file Binary contents of the file to upload
33
+ # @option args [String] :FileName What the file name is, only required when passing :file as a string
34
+ # @option args [Integer] :AlbumID SmugMug Album ID to upload the media to
35
+ # @option args [Integer, Optional] :ImageID Image ID to replace if reuploading media rather than adding new
36
+ # @option args [String, Optional] :Caption The caption for the media
37
+ # @option args [Boolean, Optional] :Hidden Whether the media should be visible
38
+ # @option args [String, Optional] :Keywords Keywords to tag the media as
39
+ # @option args [Integer, Optional] :Altitude Altitude the media was taken at
40
+ # @option args [Float, Optional] :Latitude Latitude the media was taken at
41
+ # @option args [Float, Optional] :Longitude Latitude the media was taken at
42
+ #
43
+ # @raise [SmugMug::OAuthError]
44
+ # @raise [SmugMug::HTTPError]
45
+ # @raise [SmugMug::RequestError]
46
+ # @raise [SmugMug::ReadonlyModeError]
47
+ # @raise [SmugMug::UnknownAPIError]
48
+ def upload_media(args)
49
+ raise ArgumentError, "File is required" unless args.has_key?(:file)
50
+ raise ArgumentError, "AlbumID is required" unless args.has_key?(:AlbumID)
51
+
52
+ if args[:file].is_a?(String)
53
+ args[:FileName] ||= File.basename(args[:file])
54
+ args[:content] = File.read(args[:file])
55
+ elsif args[:file].is_a?(File)
56
+ args[:FileName] ||= File.basename(args[:file].path)
57
+ args[:content] = args[:file].read
58
+ else
59
+ raise ArgumentError, "File must be a String or File"
60
+ end
61
+
62
+ args.delete(:file)
63
+ @http.request(:uploading, args)
64
+ end
65
+
66
+ ##
67
+ # Direct mapping of SmugMug 1.3.0 API, either see http://wiki.smugmug.net/display/API/API+1.3.0 or the README for examples
68
+ #
69
+ # @raise [SmugMug::OAuthError]
70
+ # @raise [SmugMug::HTTPError]
71
+ # @raise [SmugMug::RequestError]
72
+ # @raise [SmugMug::ReadonlyModeError]
73
+ # @raise [SmugMug::UnknownAPIError]
74
+ def method_missing(method, *args)
75
+ api_cat = method.to_s
76
+ return super unless SmugMug::API_METHODS[api_cat]
77
+
78
+ if klass = self.instance_variable_get("@#{api_cat}_wrapper")
79
+ klass
80
+ else
81
+ self.instance_variable_set("@#{api_cat}_wrapper", SmugMug::ApiCategory.new(@http, api_cat))
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,41 @@
1
+ module SmugMug
2
+ ##
3
+ # Generic module that provides access to the code and text separately of the exception
4
+ module ReplyErrors
5
+ attr_reader :reply_text, :reply_code
6
+
7
+ def initialize(msg, reply_code=nil, reply_text=nil)
8
+ super(msg)
9
+ @reply_code, @reply_text = reply_code, reply_text
10
+ end
11
+ end
12
+
13
+ ##
14
+ # Errors specific to the OAuth request
15
+ class OAuthError < StandardError
16
+ include ReplyErrors
17
+ end
18
+
19
+ ##
20
+ # HTTP errors
21
+ class HTTPError < StandardError
22
+ include ReplyErrors
23
+ end
24
+
25
+ ##
26
+ # Problem with the request
27
+ class RequestError < StandardError
28
+ include ReplyErrors
29
+
30
+ end
31
+
32
+ ##
33
+ # SmugMug is in read-only mode
34
+ class ReadonlyModeError < StandardError
35
+ end
36
+
37
+ ##
38
+ # Trying to call an API that doesn't exist
39
+ class UnknownAPIError < StandardError
40
+ end
41
+ end
@@ -0,0 +1,160 @@
1
+ require "cgi"
2
+ require "openssl"
3
+ require "base64"
4
+ require "net/http"
5
+ require "json"
6
+
7
+ module SmugMug
8
+ class HTTP
9
+ API_URI = URI("https://api.smugmug.com/services/api/json/1.3.0")
10
+ UPLOAD_URI = URI("http://upload.smugmug.com/")
11
+ UPLOAD_HEADERS = [:AlbumID, :Caption, :Altitude, :ImageID, :Keywords, :Latitude, :Longitude, :Hidden, :FileName]
12
+ OAUTH_ERRORS = {30 => true, 32 => true, 33 => true, 35 => true, 36 => true, 37 => true, 38 => true, 98 => true}
13
+
14
+ ##
15
+ # Creates a new HTTP wrapper to handle the network portions of the API requests
16
+ # @param [Hash] args Same as [SmugMug::HTTP]
17
+ #
18
+ def initialize(args)
19
+ @config = args
20
+ @digest = OpenSSL::Digest::Digest.new("SHA1")
21
+
22
+ @headers = {"Accept-Encoding" => "gzip"}
23
+ if args[:user_agent]
24
+ @headers["User-Agent"] = "#{args.delete(:user_agent)} (ruby-smugmug v#{SmugMug::VERSION})"
25
+ else
26
+ @headers["User-Agent"] = "Ruby-SmugMug v#{SmugMug::VERSION}"
27
+ end
28
+ end
29
+
30
+ def request(api, args)
31
+ uri = api == :uploading ? UPLOAD_URI : API_URI
32
+ args[:method] = "smugmug.#{api}" unless api == :uploading
33
+
34
+ http = ::Net::HTTP.new(uri.host, uri.port)
35
+ http.set_debug_output(@config[:debug_output]) if @config[:debug_output]
36
+
37
+ # Configure HTTPS if needed
38
+ if uri.scheme == "https"
39
+ http.use_ssl = true
40
+
41
+ if @config[:http] and @config[:http][:verify_mode]
42
+ http.verify_mode = @config[:http][:verify_mode]
43
+ http.ca_file = @config[:http][:ca_file]
44
+ http.ca_path = @config[:http][:ca_path]
45
+ else
46
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
47
+ end
48
+ end
49
+
50
+ # Upload request, which requires special handling
51
+ if api == :uploading
52
+ postdata = args.delete(:content)
53
+ headers = @headers.merge("Content-Length" => postdata.length.to_s, "Content-MD5" => Digest::MD5.hexdigest(postdata), "X-Smug-Version" => "1.3.0", "X-Smug-ResponseType" => "JSON")
54
+
55
+ UPLOAD_HEADERS.each do |key|
56
+ next unless args[key] and args[key] != ""
57
+ headers["X-Smug-#{key}"] = args[key].to_s
58
+ end
59
+
60
+ oauth = self.sign_request("POST", uri, nil)
61
+ headers["Authorization"] = "OAuth oauth_consumer_key=\"#{oauth["oauth_consumer_key"]}\", oauth_nonce=\"#{oauth["oauth_nonce"]}\", oauth_signature_method=\"#{oauth["oauth_signature_method"]}\", oauth_signature=\"#{oauth["oauth_signature"]}\", oauth_timestamp=\"#{oauth["oauth_timestamp"]}\", oauth_version=\"#{oauth["oauth_version"]}\", oauth_token=\"#{oauth["oauth_token"]}\""
62
+
63
+ # Normal API method
64
+ else
65
+ postdata = self.sign_request("POST", uri, args)
66
+ headers = @headers
67
+ end
68
+
69
+ response = http.request_post(uri.request_uri, postdata, headers)
70
+ if response.code == "204"
71
+ return nil
72
+ elsif response.code != "200"
73
+ raise SmugMug::HTTPError.new("HTTP #{response.code}, #{response.message}", response.code, response.message)
74
+ end
75
+
76
+ # Check for GZIP encoding
77
+ if response.header["content-encoding"] == "gzip"
78
+ begin
79
+ body = Zlib::GzipReader.new(StringIO.new(response.body)).read
80
+ rescue Zlib::GzipFile::Error
81
+ raise
82
+ end
83
+ else
84
+ body = response.body
85
+ end
86
+
87
+ return nil if body == ""
88
+
89
+ data = JSON.parse(body)
90
+
91
+ if data["stat"] == "fail"
92
+ # Special casing for SmugMug being in Read only mode
93
+ if data["code"] == 99
94
+ raise SmugMug::ReadonlyModeError.new("SmugMug is currently in read only mode, try again later")
95
+ end
96
+
97
+ klass = OAUTH_ERRORS[data["code"]] ? SmugMug::OAuthError : SmugMug::RequestError
98
+ raise klass.new("Error ##{data["code"]}, #{data["message"]}", data["code"], data["message"])
99
+ end
100
+
101
+ data.delete("stat")
102
+ data.delete("method")
103
+
104
+ # smugmug.albums.changeSettings at the least doesn't return any data
105
+ return nil if data.length == 0
106
+
107
+ # It seems all smugmug APIs only return one hash of data, so this should be fine and not cause issues
108
+ data.each do |_, value|
109
+ return value
110
+ end
111
+ end
112
+
113
+ ##
114
+ # Generates an OAuth signature and updates the args with the required fields
115
+ # @param [String] method HTTP method that the request is sent as
116
+ # @param [String] uri Full URL of the request
117
+ # @param [Hash] form_args Args to be passed to the server
118
+ def sign_request(method, uri, form_args)
119
+ # Convert non-string keys to strings so the sort works
120
+ args = {}
121
+ if form_args
122
+ form_args.each do |key, value|
123
+ next unless value and value != ""
124
+
125
+ key = key.to_s unless key.is_a?(String)
126
+ args[key] = value
127
+ end
128
+ end
129
+
130
+ # Add the necessary OAuth args
131
+ args["oauth_version"] = "1.0"
132
+ args["oauth_consumer_key"] = @config[:api_key]
133
+ args["oauth_nonce"] = Digest::MD5.hexdigest("#{Time.now.to_f}#{rand(10 ** 30)}")
134
+ args["oauth_signature_method"] = "HMAC-SHA1"
135
+ args["oauth_timestamp"] = Time.now.utc.to_i
136
+ args["oauth_token"] = @config[:user][:token]
137
+
138
+ # Sort the params
139
+ sorted_args = []
140
+ args.sort.each do |key, value|
141
+ sorted_args.push("#{key.to_s}=#{CGI::escape(value.to_s)}")
142
+ end
143
+
144
+ postdata = sorted_args.join("&")
145
+
146
+ # Final string to hash
147
+ sig_base = "#{method}&#{CGI::escape("#{uri.scheme}://#{uri.host}#{uri.path}")}&#{CGI::escape(postdata)}"
148
+
149
+ signature = OpenSSL::HMAC.digest(@digest, "#{@config[:oauth_secret]}&#{@config[:user][:secret]}", sig_base)
150
+ signature = CGI::escape(Base64.encode64(signature).chomp)
151
+
152
+ if uri == API_URI
153
+ "#{postdata}&oauth_signature=#{signature}"
154
+ else
155
+ args["oauth_signature"] = signature
156
+ args
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,3 @@
1
+ module SmugMug
2
+ VERSION = "0.0.1"
3
+ end
metadata ADDED
@@ -0,0 +1,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-smugmug
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Zachary Anker
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: json
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.7.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.7.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 2.8.0
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 2.8.0
46
+ description: Gem for reading and writing data from the SmugMug 1.3.0 API.
47
+ email:
48
+ - zach.anker@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - lib/ruby-smugmug.rb
54
+ - lib/smugmug/api_category.rb
55
+ - lib/smugmug/api_methods.rb
56
+ - lib/smugmug/client.rb
57
+ - lib/smugmug/exceptions.rb
58
+ - lib/smugmug/http.rb
59
+ - lib/smugmug/version.rb
60
+ - LICENSE
61
+ - README.md
62
+ - Rakefile
63
+ homepage: http://github.com/zanker/ruby-smugmug
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: 1.3.6
81
+ requirements: []
82
+ rubyforge_project: ruby-smugmug
83
+ rubygems_version: 1.8.23
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: SmugMug 1.3.0 API gem
87
+ test_files: []