ruby-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/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: []